From f6796e61f7163daa9b9546f99f787e79a35edb36 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Tue, 30 Sep 2025 16:25:33 -0400 Subject: [PATCH 1/7] Autoformat (#297) --- .github/scripts/mark_skipped.py | 10 +- .github/workflows/bump-version.yml | 2 +- .github/workflows/claude-nl-suite.yml | 1865 ++++++++--------- .github/workflows/github-repo-stats.yml | 1 - .github/workflows/unity-tests.yml | 2 +- .../UnityMCPTests/Assets/Scripts/Hello.cs | 5 - .../Scripts/LongUnityScriptClaudeTest.cs | 2 - .../Scripts/TestAsmdef/CustomComponent.cs | 4 +- .../EditMode/Tools/AIPropertyMatchingTests.cs | 38 +- .../EditMode/Tools/ComponentResolverTests.cs | 38 +- .../EditMode/Tools/ManageGameObjectTests.cs | 72 +- .../Tools/ManageScriptValidationTests.cs | 48 +- UnityMcpBridge/Editor/AssemblyInfo.cs | 2 +- .../Editor/Data/DefaultServerConfig.cs | 1 - UnityMcpBridge/Editor/External/Tommy.cs | 160 +- UnityMcpBridge/Editor/Helpers/ExecPath.cs | 4 +- .../Editor/Helpers/GameObjectSerializer.cs | 71 +- .../Editor/Helpers/McpConfigFileHelper.cs | 1 - UnityMcpBridge/Editor/Helpers/McpLog.cs | 2 - .../Editor/Helpers/PackageDetector.cs | 2 - .../Editor/Helpers/PackageInstaller.cs | 8 +- UnityMcpBridge/Editor/Helpers/PortManager.cs | 10 +- UnityMcpBridge/Editor/Helpers/Response.cs | 1 - .../Editor/Helpers/ServerInstaller.cs | 6 +- .../Editor/Helpers/ServerPathResolver.cs | 2 - .../Editor/Helpers/TelemetryHelper.cs | 56 +- .../Editor/Helpers/Vector3Helper.cs | 1 - UnityMcpBridge/Editor/MCPForUnityBridge.cs | 437 ++-- .../Editor/Tools/CommandRegistry.cs | 1 - .../Editor/Tools/ManageGameObject.cs | 327 +-- UnityMcpBridge/Editor/Tools/ManageScene.cs | 1 - UnityMcpBridge/Editor/Tools/ManageScript.cs | 1 - UnityMcpBridge/Editor/Tools/ManageShader.cs | 2 +- UnityMcpBridge/Editor/Tools/ReadConsole.cs | 11 +- .../Editor/Windows/MCPForUnityEditorWindow.cs | 992 ++++----- .../Editor/Windows/VSCodeManualSetupWindow.cs | 10 +- .../Serialization/UnityTypeConverters.cs | 6 +- UnityMcpBridge/UnityMcpServer~/src/server.py | 34 +- mcp_source.py | 24 +- test_unity_socket_framing.py | 17 +- tests/conftest.py | 1 - tests/test_edit_normalization_and_noop.py | 38 +- tests/test_edit_strict_and_warnings.py | 25 +- tests/test_find_in_file_minimal.py | 10 +- tests/test_get_sha.py | 6 +- tests/test_improved_anchor_matching.py | 65 +- tests/test_logging_stdout.py | 6 +- tests/test_manage_script_uri.py | 24 +- tests/test_read_console_truncate.py | 19 +- tests/test_read_resource_minimal.py | 8 +- tests/test_resources_api.py | 13 +- tests/test_script_tools.py | 54 +- tests/test_telemetry_endpoint_validation.py | 19 +- tests/test_telemetry_queue_worker.py | 10 +- tests/test_telemetry_subaction.py | 5 +- tests/test_transport_framing.py | 14 +- tests/test_validate_script_summary.py | 10 +- tools/stress_mcp.py | 59 +- 58 files changed, 2405 insertions(+), 2258 deletions(-) diff --git a/.github/scripts/mark_skipped.py b/.github/scripts/mark_skipped.py index d2e7ca7b..22999c49 100755 --- a/.github/scripts/mark_skipped.py +++ b/.github/scripts/mark_skipped.py @@ -29,6 +29,7 @@ r"validation error .* ctx", ] + def should_skip(msg: str) -> bool: if not msg: return False @@ -38,6 +39,7 @@ def should_skip(msg: str) -> bool: return True return False + def summarize_counts(ts: ET.Element): tests = 0 failures = 0 @@ -53,6 +55,7 @@ def summarize_counts(ts: ET.Element): skipped += 1 return tests, failures, errors, skipped + def main(path: str) -> int: if not os.path.exists(path): print(f"[mark_skipped] No JUnit at {path}; nothing to do.") @@ -79,7 +82,8 @@ def main(path: str) -> int: for n in nodes: msg = (n.get("message") or "") + "\n" + (n.text or "") if should_skip(msg): - first_match_text = (n.text or "").strip() or first_match_text + first_match_text = ( + n.text or "").strip() or first_match_text to_skip = True if to_skip: for n in nodes: @@ -98,12 +102,14 @@ def main(path: str) -> int: if changed: tree.write(path, encoding="utf-8", xml_declaration=True) - print(f"[mark_skipped] Updated {path}: converted environmental failures to skipped.") + print( + f"[mark_skipped] Updated {path}: converted environmental failures to skipped.") else: print(f"[mark_skipped] No environmental failures detected in {path}.") return 0 + if __name__ == "__main__": target = ( sys.argv[1] diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 7ce2a99e..8f562c30 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -12,7 +12,7 @@ on: - major default: patch required: true - + jobs: bump: name: "Bump version and tag" diff --git a/.github/workflows/claude-nl-suite.yml b/.github/workflows/claude-nl-suite.yml index 539263d6..09176a3a 100644 --- a/.github/workflows/claude-nl-suite.yml +++ b/.github/workflows/claude-nl-suite.yml @@ -1,969 +1,966 @@ name: Claude NL/T Full Suite (Unity live) on: [workflow_dispatch] - + permissions: - contents: read - checks: write - + contents: read + checks: write + concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: - UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f1-linux-il2cpp-3 - + UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f1-linux-il2cpp-3 + jobs: - nl-suite: - runs-on: ubuntu-latest - timeout-minutes: 60 - env: - JUNIT_OUT: reports/junit-nl-suite.xml - MD_OUT: reports/junit-nl-suite.md - - steps: - # ---------- Secrets check ---------- - - name: Detect secrets (outputs) - id: detect - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - run: | - set -e - if [ -n "$ANTHROPIC_API_KEY" ]; then echo "anthropic_ok=true" >> "$GITHUB_OUTPUT"; else echo "anthropic_ok=false" >> "$GITHUB_OUTPUT"; fi - if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ]; }; then - echo "unity_ok=true" >> "$GITHUB_OUTPUT" - else - echo "unity_ok=false" >> "$GITHUB_OUTPUT" - fi - - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # ---------- Python env for MCP server (uv) ---------- - - uses: astral-sh/setup-uv@v4 - with: - python-version: '3.11' - - - name: Install MCP server - run: | - set -eux - uv venv - echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV" - echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" - if [ -f UnityMcpBridge/UnityMcpServer~/src/pyproject.toml ]; then - uv pip install -e UnityMcpBridge/UnityMcpServer~/src - elif [ -f UnityMcpBridge/UnityMcpServer~/src/requirements.txt ]; then - uv pip install -r UnityMcpBridge/UnityMcpServer~/src/requirements.txt - elif [ -f UnityMcpBridge/UnityMcpServer~/pyproject.toml ]; then - uv pip install -e UnityMcpBridge/UnityMcpServer~/ - elif [ -f UnityMcpBridge/UnityMcpServer~/requirements.txt ]; then - uv pip install -r UnityMcpBridge/UnityMcpServer~/requirements.txt - else - echo "No MCP Python deps found (skipping)" - fi + nl-suite: + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + JUNIT_OUT: reports/junit-nl-suite.xml + MD_OUT: reports/junit-nl-suite.md + + steps: + # ---------- Secrets check ---------- + - name: Detect secrets (outputs) + id: detect + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + set -e + if [ -n "$ANTHROPIC_API_KEY" ]; then echo "anthropic_ok=true" >> "$GITHUB_OUTPUT"; else echo "anthropic_ok=false" >> "$GITHUB_OUTPUT"; fi + if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ]; }; then + echo "unity_ok=true" >> "$GITHUB_OUTPUT" + else + echo "unity_ok=false" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # ---------- Python env for MCP server (uv) ---------- + - uses: astral-sh/setup-uv@v4 + with: + python-version: "3.11" + + - name: Install MCP server + run: | + set -eux + uv venv + echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV" + echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" + if [ -f UnityMcpBridge/UnityMcpServer~/src/pyproject.toml ]; then + uv pip install -e UnityMcpBridge/UnityMcpServer~/src + elif [ -f UnityMcpBridge/UnityMcpServer~/src/requirements.txt ]; then + uv pip install -r UnityMcpBridge/UnityMcpServer~/src/requirements.txt + elif [ -f UnityMcpBridge/UnityMcpServer~/pyproject.toml ]; then + uv pip install -e UnityMcpBridge/UnityMcpServer~/ + elif [ -f UnityMcpBridge/UnityMcpServer~/requirements.txt ]; then + uv pip install -r UnityMcpBridge/UnityMcpServer~/requirements.txt + else + echo "No MCP Python deps found (skipping)" + fi + + # --- Licensing: allow both ULF and EBL when available --- + - name: Decide license sources + id: lic + shell: bash + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + run: | + set -eu + use_ulf=false; use_ebl=false + [[ -n "${UNITY_LICENSE:-}" ]] && use_ulf=true + [[ -n "${UNITY_EMAIL:-}" && -n "${UNITY_PASSWORD:-}" ]] && use_ebl=true + echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT" + echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT" + echo "has_serial=$([[ -n "${UNITY_SERIAL:-}" ]] && echo true || echo false)" >> "$GITHUB_OUTPUT" + + - name: Stage Unity .ulf license (from secret) + if: steps.lic.outputs.use_ulf == 'true' + id: ulf + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + shell: bash + run: | + set -eu + mkdir -p "$RUNNER_TEMP/unity-license-ulf" "$RUNNER_TEMP/unity-local/Unity" + f="$RUNNER_TEMP/unity-license-ulf/Unity_lic.ulf" + if printf "%s" "$UNITY_LICENSE" | base64 -d - >/dev/null 2>&1; then + printf "%s" "$UNITY_LICENSE" | base64 -d - > "$f" + else + printf "%s" "$UNITY_LICENSE" > "$f" + fi + chmod 600 "$f" || true + # If someone pasted an entitlement XML into UNITY_LICENSE by mistake, re-home it: + if head -c 100 "$f" | grep -qi '<\?xml'; then + mkdir -p "$RUNNER_TEMP/unity-config/Unity/licenses" + mv "$f" "$RUNNER_TEMP/unity-config/Unity/licenses/UnityEntitlementLicense.xml" + echo "ok=false" >> "$GITHUB_OUTPUT" + elif grep -qi '' "$f"; then + # provide it in the standard local-share path too + cp -f "$f" "$RUNNER_TEMP/unity-local/Unity/Unity_lic.ulf" + echo "ok=true" >> "$GITHUB_OUTPUT" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + fi + + # --- Activate via EBL inside the same Unity image (writes host-side entitlement) --- + - name: Activate Unity (EBL via container - host-mount) + if: steps.lic.outputs.use_ebl == 'true' + shell: bash + env: + UNITY_IMAGE: ${{ env.UNITY_IMAGE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + run: | + set -euxo pipefail + # host dirs to receive the full Unity config and local-share + mkdir -p "$RUNNER_TEMP/unity-config" "$RUNNER_TEMP/unity-local" + + # Try Pro first if serial is present, otherwise named-user EBL. + docker run --rm --network host \ + -e HOME=/root \ + -e UNITY_EMAIL -e UNITY_PASSWORD -e UNITY_SERIAL \ + -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \ + -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \ + "$UNITY_IMAGE" bash -lc ' + set -euxo pipefail + if [[ -n "${UNITY_SERIAL:-}" ]]; then + /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ + -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true + else + /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ + -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -quit || true + fi + ls -la /root/.config/unity3d/Unity/licenses || true + ' - # --- Licensing: allow both ULF and EBL when available --- - - name: Decide license sources - id: lic - shell: bash - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - run: | - set -eu - use_ulf=false; use_ebl=false - [[ -n "${UNITY_LICENSE:-}" ]] && use_ulf=true - [[ -n "${UNITY_EMAIL:-}" && -n "${UNITY_PASSWORD:-}" ]] && use_ebl=true - echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT" - echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT" - echo "has_serial=$([[ -n "${UNITY_SERIAL:-}" ]] && echo true || echo false)" >> "$GITHUB_OUTPUT" - - - name: Stage Unity .ulf license (from secret) - if: steps.lic.outputs.use_ulf == 'true' - id: ulf - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - shell: bash - run: | - set -eu - mkdir -p "$RUNNER_TEMP/unity-license-ulf" "$RUNNER_TEMP/unity-local/Unity" - f="$RUNNER_TEMP/unity-license-ulf/Unity_lic.ulf" - if printf "%s" "$UNITY_LICENSE" | base64 -d - >/dev/null 2>&1; then - printf "%s" "$UNITY_LICENSE" | base64 -d - > "$f" + # Verify entitlement written to host mount; allow ULF-only runs to proceed + if ! find "$RUNNER_TEMP/unity-config" -type f -iname "*.xml" | grep -q .; then + if [[ "${{ steps.ulf.outputs.ok }}" == "true" ]]; then + echo "EBL entitlement not found; proceeding with ULF-only (ok=true)." else - printf "%s" "$UNITY_LICENSE" > "$f" + echo "No entitlement produced and no valid ULF; cannot continue." >&2 + exit 1 fi - chmod 600 "$f" || true - # If someone pasted an entitlement XML into UNITY_LICENSE by mistake, re-home it: - if head -c 100 "$f" | grep -qi '<\?xml'; then - mkdir -p "$RUNNER_TEMP/unity-config/Unity/licenses" - mv "$f" "$RUNNER_TEMP/unity-config/Unity/licenses/UnityEntitlementLicense.xml" - echo "ok=false" >> "$GITHUB_OUTPUT" - elif grep -qi '' "$f"; then - # provide it in the standard local-share path too - cp -f "$f" "$RUNNER_TEMP/unity-local/Unity/Unity_lic.ulf" - echo "ok=true" >> "$GITHUB_OUTPUT" - else - echo "ok=false" >> "$GITHUB_OUTPUT" + fi + + # EBL entitlement is already written directly to $RUNNER_TEMP/unity-config by the activation step + + # ---------- Warm up project (import Library once) ---------- + - name: Warm up project (import Library once) + if: steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true' + shell: bash + env: + UNITY_IMAGE: ${{ env.UNITY_IMAGE }} + ULF_OK: ${{ steps.ulf.outputs.ok }} + run: | + set -euxo pipefail + manual_args=() + if [[ "${ULF_OK:-false}" == "true" ]]; then + manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf") + fi + docker run --rm --network host \ + -e HOME=/root \ + -v "${{ github.workspace }}:/workspace" -w /workspace \ + -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \ + -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \ + "$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ + -projectPath /workspace/TestProjects/UnityMCPTests \ + "${manual_args[@]}" \ + -quit + + # ---------- Clean old MCP status ---------- + - name: Clean old MCP status + run: | + set -eux + mkdir -p "$HOME/.unity-mcp" + rm -f "$HOME/.unity-mcp"/unity-mcp-status-*.json || true + + # ---------- Start headless Unity (persistent bridge) ---------- + - name: Start Unity (persistent bridge) + if: steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true' + shell: bash + env: + UNITY_IMAGE: ${{ env.UNITY_IMAGE }} + ULF_OK: ${{ steps.ulf.outputs.ok }} + run: | + set -euxo pipefail + manual_args=() + if [[ "${ULF_OK:-false}" == "true" ]]; then + manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf") + fi + + mkdir -p "$RUNNER_TEMP/unity-status" + docker rm -f unity-mcp >/dev/null 2>&1 || true + docker run -d --name unity-mcp --network host \ + -e HOME=/root \ + -e UNITY_MCP_ALLOW_BATCH=1 \ + -e UNITY_MCP_STATUS_DIR=/root/.unity-mcp \ + -e UNITY_MCP_BIND_HOST=127.0.0.1 \ + -v "${{ github.workspace }}:/workspace" -w /workspace \ + -v "$RUNNER_TEMP/unity-status:/root/.unity-mcp" \ + -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d:ro" \ + -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d:ro" \ + "$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ + -stackTraceLogType Full \ + -projectPath /workspace/TestProjects/UnityMCPTests \ + "${manual_args[@]}" \ + -executeMethod MCPForUnity.Editor.MCPForUnityBridge.StartAutoConnect + + # ---------- Wait for Unity bridge ---------- + - name: Wait for Unity bridge (robust) + shell: bash + run: | + set -euo pipefail + deadline=$((SECONDS+900)) # 15 min max + fatal_after=$((SECONDS+120)) # give licensing 2 min to settle + + # Fail fast only if container actually died + st="$(docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' unity-mcp 2>/dev/null || true)" + case "$st" in exited*|dead*) docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'; exit 1;; esac + + # Patterns + ok_pat='(Bridge|MCP(For)?Unity|AutoConnect).*(listening|ready|started|port|bound)' + # Only truly fatal signals; allow transient "Licensing::..." chatter + license_fatal='No valid Unity|License is not active|cannot load ULF|Signature element not found|Token not found|0 entitlement|Entitlement.*(failed|denied)|License (activation|return|renewal).*(failed|expired|denied)' + + while [ $SECONDS -lt $deadline ]; do + logs="$(docker logs unity-mcp 2>&1 || true)" + + # 1) Primary: status JSON exposes TCP port + port="$(jq -r '.unity_port // empty' "$RUNNER_TEMP"/unity-status/unity-mcp-status-*.json 2>/dev/null | head -n1 || true)" + if [[ -n "${port:-}" ]] && timeout 1 bash -lc "exec 3<>/dev/tcp/127.0.0.1/$port"; then + echo "Bridge ready on port $port" + exit 0 fi - # --- Activate via EBL inside the same Unity image (writes host-side entitlement) --- - - name: Activate Unity (EBL via container - host-mount) - if: steps.lic.outputs.use_ebl == 'true' - shell: bash - env: - UNITY_IMAGE: ${{ env.UNITY_IMAGE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - run: | - set -euxo pipefail - # host dirs to receive the full Unity config and local-share - mkdir -p "$RUNNER_TEMP/unity-config" "$RUNNER_TEMP/unity-local" - - # Try Pro first if serial is present, otherwise named-user EBL. - docker run --rm --network host \ - -e HOME=/root \ - -e UNITY_EMAIL -e UNITY_PASSWORD -e UNITY_SERIAL \ - -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \ - -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \ - "$UNITY_IMAGE" bash -lc ' - set -euxo pipefail - if [[ -n "${UNITY_SERIAL:-}" ]]; then - /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ - -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true - else - /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ - -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -quit || true - fi - ls -la /root/.config/unity3d/Unity/licenses || true - ' - - # Verify entitlement written to host mount; allow ULF-only runs to proceed - if ! find "$RUNNER_TEMP/unity-config" -type f -iname "*.xml" | grep -q .; then - if [[ "${{ steps.ulf.outputs.ok }}" == "true" ]]; then - echo "EBL entitlement not found; proceeding with ULF-only (ok=true)." - else - echo "No entitlement produced and no valid ULF; cannot continue." >&2 - exit 1 - fi + # 2) Secondary: log markers + if echo "$logs" | grep -qiE "$ok_pat"; then + echo "Bridge ready (log markers)" + exit 0 fi - # EBL entitlement is already written directly to $RUNNER_TEMP/unity-config by the activation step - - # ---------- Warm up project (import Library once) ---------- - - name: Warm up project (import Library once) - if: steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true' - shell: bash - env: - UNITY_IMAGE: ${{ env.UNITY_IMAGE }} - ULF_OK: ${{ steps.ulf.outputs.ok }} - run: | - set -euxo pipefail - manual_args=() - if [[ "${ULF_OK:-false}" == "true" ]]; then - manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf") - fi - docker run --rm --network host \ - -e HOME=/root \ - -v "${{ github.workspace }}:/workspace" -w /workspace \ - -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \ - -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \ - "$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ - -projectPath /workspace/TestProjects/UnityMCPTests \ - "${manual_args[@]}" \ - -quit - - # ---------- Clean old MCP status ---------- - - name: Clean old MCP status - run: | - set -eux - mkdir -p "$HOME/.unity-mcp" - rm -f "$HOME/.unity-mcp"/unity-mcp-status-*.json || true - - # ---------- Start headless Unity (persistent bridge) ---------- - - name: Start Unity (persistent bridge) - if: steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true' - shell: bash - env: - UNITY_IMAGE: ${{ env.UNITY_IMAGE }} - ULF_OK: ${{ steps.ulf.outputs.ok }} - run: | - set -euxo pipefail - manual_args=() - if [[ "${ULF_OK:-false}" == "true" ]]; then - manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf") + # Only treat license failures as fatal *after* warm-up + if [ $SECONDS -ge $fatal_after ] && echo "$logs" | grep -qiE "$license_fatal"; then + echo "::error::Fatal licensing signal detected after warm-up" + echo "$logs" | tail -n 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' + exit 1 fi - mkdir -p "$RUNNER_TEMP/unity-status" - docker rm -f unity-mcp >/dev/null 2>&1 || true - docker run -d --name unity-mcp --network host \ - -e HOME=/root \ - -e UNITY_MCP_ALLOW_BATCH=1 \ - -e UNITY_MCP_STATUS_DIR=/root/.unity-mcp \ - -e UNITY_MCP_BIND_HOST=127.0.0.1 \ - -v "${{ github.workspace }}:/workspace" -w /workspace \ - -v "$RUNNER_TEMP/unity-status:/root/.unity-mcp" \ - -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d:ro" \ - -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d:ro" \ - "$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ - -stackTraceLogType Full \ - -projectPath /workspace/TestProjects/UnityMCPTests \ - "${manual_args[@]}" \ - -executeMethod MCPForUnity.Editor.MCPForUnityBridge.StartAutoConnect - - # ---------- Wait for Unity bridge ---------- - - name: Wait for Unity bridge (robust) - shell: bash - run: | - set -euo pipefail - deadline=$((SECONDS+900)) # 15 min max - fatal_after=$((SECONDS+120)) # give licensing 2 min to settle - - # Fail fast only if container actually died - st="$(docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' unity-mcp 2>/dev/null || true)" - case "$st" in exited*|dead*) docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'; exit 1;; esac - - # Patterns - ok_pat='(Bridge|MCP(For)?Unity|AutoConnect).*(listening|ready|started|port|bound)' - # Only truly fatal signals; allow transient "Licensing::..." chatter - license_fatal='No valid Unity|License is not active|cannot load ULF|Signature element not found|Token not found|0 entitlement|Entitlement.*(failed|denied)|License (activation|return|renewal).*(failed|expired|denied)' - - while [ $SECONDS -lt $deadline ]; do - logs="$(docker logs unity-mcp 2>&1 || true)" - - # 1) Primary: status JSON exposes TCP port - port="$(jq -r '.unity_port // empty' "$RUNNER_TEMP"/unity-status/unity-mcp-status-*.json 2>/dev/null | head -n1 || true)" - if [[ -n "${port:-}" ]] && timeout 1 bash -lc "exec 3<>/dev/tcp/127.0.0.1/$port"; then - echo "Bridge ready on port $port" - exit 0 - fi - - # 2) Secondary: log markers - if echo "$logs" | grep -qiE "$ok_pat"; then - echo "Bridge ready (log markers)" - exit 0 - fi - - # Only treat license failures as fatal *after* warm-up - if [ $SECONDS -ge $fatal_after ] && echo "$logs" | grep -qiE "$license_fatal"; then - echo "::error::Fatal licensing signal detected after warm-up" - echo "$logs" | tail -n 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' - exit 1 - fi - - # If the container dies mid-wait, bail - st="$(docker inspect -f '{{.State.Status}}' unity-mcp 2>/dev/null || true)" - if [[ "$st" != "running" ]]; then - echo "::error::Unity container exited during wait"; docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' - exit 1 - fi - - sleep 2 - done - - echo "::error::Bridge not ready before deadline" - docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' - exit 1 + # If the container dies mid-wait, bail + st="$(docker inspect -f '{{.State.Status}}' unity-mcp 2>/dev/null || true)" + if [[ "$st" != "running" ]]; then + echo "::error::Unity container exited during wait"; docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' + exit 1 + fi - # (moved) — return license after Unity is stopped - - # ---------- MCP client config ---------- - - name: Write MCP config (.claude/mcp.json) - run: | - set -eux - mkdir -p .claude - cat > .claude/mcp.json < .claude/mcp.json < .claude/settings.json <<'JSON' - { - "permissions": { - "allow": [ - "mcp__unity", - "Edit(reports/**)" - ], - "deny": [ - "Bash", - "MultiEdit", - "WebFetch", - "WebSearch", - "Task", - "TodoWrite", - "NotebookEdit", - "NotebookRead" - ] - } + } + JSON + + - name: Pin Claude tool permissions (.claude/settings.json) + run: | + set -eux + mkdir -p .claude + cat > .claude/settings.json <<'JSON' + { + "permissions": { + "allow": [ + "mcp__unity", + "Edit(reports/**)" + ], + "deny": [ + "Bash", + "MultiEdit", + "WebFetch", + "WebSearch", + "Task", + "TodoWrite", + "NotebookEdit", + "NotebookRead" + ] } - JSON - - # ---------- Reports & helper ---------- - - name: Prepare reports and dirs - run: | - set -eux - rm -f reports/*.xml reports/*.md || true - mkdir -p reports reports/_snapshots reports/_staging - - - name: Create report skeletons - run: | - set -eu - cat > "$JUNIT_OUT" <<'XML' - - - - Bootstrap placeholder; suite will append real tests. - - - XML - printf '# Unity NL/T Editing Suite Test Results\n\n' > "$MD_OUT" - - - name: Verify Unity bridge status/port - run: | - set -euxo pipefail - ls -la "$RUNNER_TEMP/unity-status" || true - jq -r . "$RUNNER_TEMP"/unity-status/unity-mcp-status-*.json | sed -n '1,80p' || true - - shopt -s nullglob - status_files=("$RUNNER_TEMP"/unity-status/unity-mcp-status-*.json) - if ((${#status_files[@]})); then - port="$(grep -hEo '"unity_port"[[:space:]]*:[[:space:]]*[0-9]+' "${status_files[@]}" \ - | sed -E 's/.*: *([0-9]+).*/\1/' | head -n1 || true)" - else - port="" - fi - - echo "unity_port=$port" - if [[ -n "$port" ]]; then - timeout 1 bash -lc "exec 3<>/dev/tcp/127.0.0.1/$port" && echo "TCP OK" - fi - - # (removed) Revert helper and baseline snapshot are no longer used - - - # ---------- Run suite in two passes ---------- - - name: Run Claude NL pass - uses: anthropics/claude-code-base-action@beta - if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true' - continue-on-error: true - with: - use_node_cache: false - prompt_file: .claude/prompts/nl-unity-suite-nl.md - mcp_config: .claude/mcp.json - settings: .claude/settings.json - allowed_tools: "mcp__unity,Edit(reports/**),MultiEdit(reports/**)" - disallowed_tools: "Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead" - model: claude-3-7-sonnet-20250219 - append_system_prompt: | - You are running the NL pass only. - - Emit exactly NL-0, NL-1, NL-2, NL-3, NL-4. - - Write each to reports/${ID}_results.xml. - - Prefer a single MultiEdit(reports/**) batch. Do not emit any T-* tests. - - Stop after NL-4_results.xml is written. - timeout_minutes: "30" - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - - - name: Run Claude T pass A-J - uses: anthropics/claude-code-base-action@beta - if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true' - continue-on-error: true - with: - use_node_cache: false - prompt_file: .claude/prompts/nl-unity-suite-t.md - mcp_config: .claude/mcp.json - settings: .claude/settings.json - allowed_tools: "mcp__unity,Edit(reports/**),MultiEdit(reports/**)" - disallowed_tools: "Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead" - model: claude-3-5-haiku-20241022 - append_system_prompt: | - You are running the T pass (A–J) only. - Output requirements: - - Emit exactly 10 test fragments: T-A, T-B, T-C, T-D, T-E, T-F, T-G, T-H, T-I, T-J. - - Write each fragment to reports/${ID}_results.xml (e.g., T-A_results.xml). - - Prefer a single MultiEdit(reports/**) call that writes all ten files in one batch. - - If MultiEdit is not used, emit individual writes for any missing IDs until all ten exist. - - Do not emit any NL-* fragments. - Stop condition: - - After T-J_results.xml is written, stop. - timeout_minutes: "30" - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # (moved) Assert T coverage after staged fragments are promoted - - - name: Check T coverage incomplete (pre-retry) - id: t_cov - if: always() - shell: bash - run: | - set -euo pipefail - missing=() - for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do - if [[ ! -s "reports/${id}_results.xml" && ! -s "reports/_staging/${id}_results.xml" ]]; then - missing+=("$id") - fi - done - echo "missing=${#missing[@]}" >> "$GITHUB_OUTPUT" - if (( ${#missing[@]} )); then - echo "list=${missing[*]}" >> "$GITHUB_OUTPUT" - fi - - - name: Retry T pass (Sonnet) if incomplete - if: steps.t_cov.outputs.missing != '0' - uses: anthropics/claude-code-base-action@beta - with: - use_node_cache: false - prompt_file: .claude/prompts/nl-unity-suite-t.md - mcp_config: .claude/mcp.json - settings: .claude/settings.json - allowed_tools: "mcp__unity,Edit(reports/**),MultiEdit(reports/**)" - disallowed_tools: "Bash,MultiEdit(/!(reports/**)),WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead" - model: claude-3-7-sonnet-20250219 - fallback_model: claude-3-5-haiku-20241022 - append_system_prompt: | - You are running the T pass only. - Output requirements: - - Emit exactly 10 test fragments: T-A, T-B, T-C, T-D, T-E, T-F, T-G, T-H, T-I, T-J. - - Write each fragment to reports/${ID}_results.xml (e.g., T-A_results.xml). - - Prefer a single MultiEdit(reports/**) call that writes all ten files in one batch. - - If MultiEdit is not used, emit individual writes for any missing IDs until all ten exist. - - Do not emit any NL-* fragments. - Stop condition: - - After T-J_results.xml is written, stop. - timeout_minutes: "30" - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - - name: Re-assert T coverage (post-retry) - if: always() - shell: bash - run: | - set -euo pipefail - missing=() - for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do - [[ -s "reports/${id}_results.xml" ]] || missing+=("$id") - done - if (( ${#missing[@]} )); then - echo "::error::Still missing T fragments: ${missing[*]}" - exit 1 + } + JSON + + # ---------- Reports & helper ---------- + - name: Prepare reports and dirs + run: | + set -eux + rm -f reports/*.xml reports/*.md || true + mkdir -p reports reports/_snapshots reports/_staging + + - name: Create report skeletons + run: | + set -eu + cat > "$JUNIT_OUT" <<'XML' + + + + Bootstrap placeholder; suite will append real tests. + + + XML + printf '# Unity NL/T Editing Suite Test Results\n\n' > "$MD_OUT" + + - name: Verify Unity bridge status/port + run: | + set -euxo pipefail + ls -la "$RUNNER_TEMP/unity-status" || true + jq -r . "$RUNNER_TEMP"/unity-status/unity-mcp-status-*.json | sed -n '1,80p' || true + + shopt -s nullglob + status_files=("$RUNNER_TEMP"/unity-status/unity-mcp-status-*.json) + if ((${#status_files[@]})); then + port="$(grep -hEo '"unity_port"[[:space:]]*:[[:space:]]*[0-9]+' "${status_files[@]}" \ + | sed -E 's/.*: *([0-9]+).*/\1/' | head -n1 || true)" + else + port="" + fi + + echo "unity_port=$port" + if [[ -n "$port" ]]; then + timeout 1 bash -lc "exec 3<>/dev/tcp/127.0.0.1/$port" && echo "TCP OK" + fi + + # (removed) Revert helper and baseline snapshot are no longer used + + # ---------- Run suite in two passes ---------- + - name: Run Claude NL pass + uses: anthropics/claude-code-base-action@beta + if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true' + continue-on-error: true + with: + use_node_cache: false + prompt_file: .claude/prompts/nl-unity-suite-nl.md + mcp_config: .claude/mcp.json + settings: .claude/settings.json + allowed_tools: "mcp__unity,Edit(reports/**),MultiEdit(reports/**)" + disallowed_tools: "Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead" + model: claude-3-7-sonnet-20250219 + append_system_prompt: | + You are running the NL pass only. + - Emit exactly NL-0, NL-1, NL-2, NL-3, NL-4. + - Write each to reports/${ID}_results.xml. + - Prefer a single MultiEdit(reports/**) batch. Do not emit any T-* tests. + - Stop after NL-4_results.xml is written. + timeout_minutes: "30" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + + - name: Run Claude T pass A-J + uses: anthropics/claude-code-base-action@beta + if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true' + continue-on-error: true + with: + use_node_cache: false + prompt_file: .claude/prompts/nl-unity-suite-t.md + mcp_config: .claude/mcp.json + settings: .claude/settings.json + allowed_tools: "mcp__unity,Edit(reports/**),MultiEdit(reports/**)" + disallowed_tools: "Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead" + model: claude-3-5-haiku-20241022 + append_system_prompt: | + You are running the T pass (A–J) only. + Output requirements: + - Emit exactly 10 test fragments: T-A, T-B, T-C, T-D, T-E, T-F, T-G, T-H, T-I, T-J. + - Write each fragment to reports/${ID}_results.xml (e.g., T-A_results.xml). + - Prefer a single MultiEdit(reports/**) call that writes all ten files in one batch. + - If MultiEdit is not used, emit individual writes for any missing IDs until all ten exist. + - Do not emit any NL-* fragments. + Stop condition: + - After T-J_results.xml is written, stop. + timeout_minutes: "30" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + + # (moved) Assert T coverage after staged fragments are promoted + + - name: Check T coverage incomplete (pre-retry) + id: t_cov + if: always() + shell: bash + run: | + set -euo pipefail + missing=() + for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do + if [[ ! -s "reports/${id}_results.xml" && ! -s "reports/_staging/${id}_results.xml" ]]; then + missing+=("$id") fi - - # (kept) Finalize staged report fragments (promote to reports/) - - # (removed duplicate) Finalize staged report fragments - - - name: Assert T coverage (after promotion) - if: always() - shell: bash - run: | - set -euo pipefail - missing=() - for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do - if [[ ! -s "reports/${id}_results.xml" ]]; then - # Accept staged fragment as present - [[ -s "reports/_staging/${id}_results.xml" ]] || missing+=("$id") - fi - done - if (( ${#missing[@]} )); then - echo "::error::Missing T fragments: ${missing[*]}" - exit 1 + done + echo "missing=${#missing[@]}" >> "$GITHUB_OUTPUT" + if (( ${#missing[@]} )); then + echo "list=${missing[*]}" >> "$GITHUB_OUTPUT" + fi + + - name: Retry T pass (Sonnet) if incomplete + if: steps.t_cov.outputs.missing != '0' + uses: anthropics/claude-code-base-action@beta + with: + use_node_cache: false + prompt_file: .claude/prompts/nl-unity-suite-t.md + mcp_config: .claude/mcp.json + settings: .claude/settings.json + allowed_tools: "mcp__unity,Edit(reports/**),MultiEdit(reports/**)" + disallowed_tools: "Bash,MultiEdit(/!(reports/**)),WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead" + model: claude-3-7-sonnet-20250219 + fallback_model: claude-3-5-haiku-20241022 + append_system_prompt: | + You are running the T pass only. + Output requirements: + - Emit exactly 10 test fragments: T-A, T-B, T-C, T-D, T-E, T-F, T-G, T-H, T-I, T-J. + - Write each fragment to reports/${ID}_results.xml (e.g., T-A_results.xml). + - Prefer a single MultiEdit(reports/**) call that writes all ten files in one batch. + - If MultiEdit is not used, emit individual writes for any missing IDs until all ten exist. + - Do not emit any NL-* fragments. + Stop condition: + - After T-J_results.xml is written, stop. + timeout_minutes: "30" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + + - name: Re-assert T coverage (post-retry) + if: always() + shell: bash + run: | + set -euo pipefail + missing=() + for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do + [[ -s "reports/${id}_results.xml" ]] || missing+=("$id") + done + if (( ${#missing[@]} )); then + echo "::error::Still missing T fragments: ${missing[*]}" + exit 1 + fi + + # (kept) Finalize staged report fragments (promote to reports/) + + # (removed duplicate) Finalize staged report fragments + + - name: Assert T coverage (after promotion) + if: always() + shell: bash + run: | + set -euo pipefail + missing=() + for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do + if [[ ! -s "reports/${id}_results.xml" ]]; then + # Accept staged fragment as present + [[ -s "reports/_staging/${id}_results.xml" ]] || missing+=("$id") fi - - - name: Canonicalize testcase names (NL/T prefixes) - if: always() - shell: bash - run: | - python3 - <<'PY' - from pathlib import Path - import xml.etree.ElementTree as ET, re, os - - RULES = [ - ("NL-0", r"\b(NL-0|Baseline|State\s*Capture)\b"), - ("NL-1", r"\b(NL-1|Core\s*Method)\b"), - ("NL-2", r"\b(NL-2|Anchor|Build\s*marker)\b"), - ("NL-3", r"\b(NL-3|End[-\s]*of[-\s]*Class\s*Content|Tail\s*test\s*[ABC])\b"), - ("NL-4", r"\b(NL-4|Console|Unity\s*console)\b"), - ("T-A", r"\b(T-?A|Temporary\s*Helper)\b"), - ("T-B", r"\b(T-?B|Method\s*Body\s*Interior)\b"), - ("T-C", r"\b(T-?C|Different\s*Method\s*Interior|ApplyBlend)\b"), - ("T-D", r"\b(T-?D|End[-\s]*of[-\s]*Class\s*Helper|TestHelper)\b"), - ("T-E", r"\b(T-?E|Method\s*Evolution|Counter|IncrementCounter)\b"), - ("T-F", r"\b(T-?F|Atomic\s*Multi[-\s]*Edit)\b"), - ("T-G", r"\b(T-?G|Path\s*Normalization)\b"), - ("T-H", r"\b(T-?H|Validation\s*on\s*Modified)\b"), - ("T-I", r"\b(T-?I|Failure\s*Surface)\b"), - ("T-J", r"\b(T-?J|Idempotenc(y|e))\b"), - ] - - def canon_name(name: str) -> str: - n = name or "" - for tid, pat in RULES: - if re.search(pat, n, flags=re.I): - # If it already starts with the correct format, leave it alone - if re.match(rf'^\s*{re.escape(tid)}\s*[—–-]', n, flags=re.I): - return n.strip() - # If it has a different separator, extract title and reformat - title_match = re.search(rf'{re.escape(tid)}\s*[:.\-–—]\s*(.+)', n, flags=re.I) - if title_match: - title = title_match.group(1).strip() - return f"{tid} — {title}" - # Otherwise, just return the canonical ID - return tid - return n - - def id_from_filename(p: Path): + done + if (( ${#missing[@]} )); then + echo "::error::Missing T fragments: ${missing[*]}" + exit 1 + fi + + - name: Canonicalize testcase names (NL/T prefixes) + if: always() + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + import xml.etree.ElementTree as ET, re, os + + RULES = [ + ("NL-0", r"\b(NL-0|Baseline|State\s*Capture)\b"), + ("NL-1", r"\b(NL-1|Core\s*Method)\b"), + ("NL-2", r"\b(NL-2|Anchor|Build\s*marker)\b"), + ("NL-3", r"\b(NL-3|End[-\s]*of[-\s]*Class\s*Content|Tail\s*test\s*[ABC])\b"), + ("NL-4", r"\b(NL-4|Console|Unity\s*console)\b"), + ("T-A", r"\b(T-?A|Temporary\s*Helper)\b"), + ("T-B", r"\b(T-?B|Method\s*Body\s*Interior)\b"), + ("T-C", r"\b(T-?C|Different\s*Method\s*Interior|ApplyBlend)\b"), + ("T-D", r"\b(T-?D|End[-\s]*of[-\s]*Class\s*Helper|TestHelper)\b"), + ("T-E", r"\b(T-?E|Method\s*Evolution|Counter|IncrementCounter)\b"), + ("T-F", r"\b(T-?F|Atomic\s*Multi[-\s]*Edit)\b"), + ("T-G", r"\b(T-?G|Path\s*Normalization)\b"), + ("T-H", r"\b(T-?H|Validation\s*on\s*Modified)\b"), + ("T-I", r"\b(T-?I|Failure\s*Surface)\b"), + ("T-J", r"\b(T-?J|Idempotenc(y|e))\b"), + ] + + def canon_name(name: str) -> str: + n = name or "" + for tid, pat in RULES: + if re.search(pat, n, flags=re.I): + # If it already starts with the correct format, leave it alone + if re.match(rf'^\s*{re.escape(tid)}\s*[—–-]', n, flags=re.I): + return n.strip() + # If it has a different separator, extract title and reformat + title_match = re.search(rf'{re.escape(tid)}\s*[:.\-–—]\s*(.+)', n, flags=re.I) + if title_match: + title = title_match.group(1).strip() + return f"{tid} — {title}" + # Otherwise, just return the canonical ID + return tid + return n + + def id_from_filename(p: Path): + n = p.name + m = re.match(r'NL(\d+)_results\.xml$', n, re.I) + if m: + return f"NL-{int(m.group(1))}" + m = re.match(r'T([A-J])_results\.xml$', n, re.I) + if m: + return f"T-{m.group(1).upper()}" + return None + + frags = list(sorted(Path("reports").glob("*_results.xml"))) + for frag in frags: + try: + tree = ET.parse(frag); root = tree.getroot() + except Exception: + continue + if root.tag != "testcase": + continue + file_id = id_from_filename(frag) + old = root.get("name") or "" + # Prefer filename-derived ID; if name doesn't start with it, override + if file_id: + # Respect file's ID (prevents T-D being renamed to NL-3 by loose patterns) + title = re.sub(r'^\s*(NL-\d+|T-[A-Z])\s*[—–:\-]\s*', '', old).strip() + new = f"{file_id} — {title}" if title else file_id + else: + new = canon_name(old) + if new != old and new: + root.set("name", new) + tree.write(frag, encoding="utf-8", xml_declaration=False) + print(f'canon: {frag.name}: "{old}" -> "{new}"') + + # Note: Do not auto-relable fragments. We rely on per-test strict emission + # and the backfill step to surface missing tests explicitly. + PY + + - name: Backfill missing NL/T tests (fail placeholders) + if: always() + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + import xml.etree.ElementTree as ET + import re + + DESIRED = ["NL-0","NL-1","NL-2","NL-3","NL-4","T-A","T-B","T-C","T-D","T-E","T-F","T-G","T-H","T-I","T-J"] + seen = set() + def id_from_filename(p: Path): + n = p.name + m = re.match(r'NL(\d+)_results\.xml$', n, re.I) + if m: + return f"NL-{int(m.group(1))}" + m = re.match(r'T([A-J])_results\.xml$', n, re.I) + if m: + return f"T-{m.group(1).upper()}" + return None + + for p in Path("reports").glob("*_results.xml"): + try: + r = ET.parse(p).getroot() + except Exception: + continue + # Count by filename id primarily; fall back to testcase name if needed + fid = id_from_filename(p) + if fid in DESIRED: + seen.add(fid) + continue + if r.tag == "testcase": + name = (r.get("name") or "").strip() + for d in DESIRED: + if name.startswith(d): + seen.add(d) + break + + Path("reports").mkdir(parents=True, exist_ok=True) + for d in DESIRED: + if d in seen: + continue + frag = Path(f"reports/{d}_results.xml") + tc = ET.Element("testcase", {"classname":"UnityMCP.NL-T", "name": d}) + fail = ET.SubElement(tc, "failure", {"message":"not produced"}) + fail.text = "The agent did not emit a fragment for this test." + ET.ElementTree(tc).write(frag, encoding="utf-8", xml_declaration=False) + print(f"backfill: {d}") + PY + + - name: "Debug: list testcase names" + if: always() + run: | + python3 - <<'PY' + from pathlib import Path + import xml.etree.ElementTree as ET + for p in sorted(Path('reports').glob('*_results.xml')): + try: + r = ET.parse(p).getroot() + if r.tag == 'testcase': + print(f"{p.name}: {(r.get('name') or '').strip()}") + except Exception: + pass + PY + + # ---------- Merge testcase fragments into JUnit ---------- + - name: Normalize/assemble JUnit in-place (single file) + if: always() + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + import xml.etree.ElementTree as ET + import re, os + + def localname(tag: str) -> str: + return tag.rsplit('}', 1)[-1] if '}' in tag else tag + + src = Path(os.environ.get('JUNIT_OUT', 'reports/junit-nl-suite.xml')) + if not src.exists(): + raise SystemExit(0) + + tree = ET.parse(src) + root = tree.getroot() + suite = root.find('./*') if localname(root.tag) == 'testsuites' else root + if suite is None: + raise SystemExit(0) + + def id_from_filename(p: Path): n = p.name m = re.match(r'NL(\d+)_results\.xml$', n, re.I) if m: - return f"NL-{int(m.group(1))}" + return f"NL-{int(m.group(1))}" m = re.match(r'T([A-J])_results\.xml$', n, re.I) if m: - return f"T-{m.group(1).upper()}" + return f"T-{m.group(1).upper()}" + return None + + def id_from_system_out(tc): + so = tc.find('system-out') + if so is not None and so.text: + m = re.search(r'\b(NL-\d+|T-[A-Z])\b', so.text) + if m: + return m.group(1) return None - frags = list(sorted(Path("reports").glob("*_results.xml"))) - for frag in frags: + fragments = sorted(Path('reports').glob('*_results.xml')) + added = 0 + renamed = 0 + + for frag in fragments: + tcs = [] try: - tree = ET.parse(frag); root = tree.getroot() + froot = ET.parse(frag).getroot() + if localname(froot.tag) == 'testcase': + tcs = [froot] + else: + tcs = list(froot.findall('.//testcase')) except Exception: - continue - if root.tag != "testcase": - continue - file_id = id_from_filename(frag) - old = root.get("name") or "" - # Prefer filename-derived ID; if name doesn't start with it, override - if file_id: - # Respect file's ID (prevents T-D being renamed to NL-3 by loose patterns) - title = re.sub(r'^\s*(NL-\d+|T-[A-Z])\s*[—–:\-]\s*', '', old).strip() - new = f"{file_id} — {title}" if title else file_id - else: - new = canon_name(old) - if new != old and new: - root.set("name", new) - tree.write(frag, encoding="utf-8", xml_declaration=False) - print(f'canon: {frag.name}: "{old}" -> "{new}"') - - # Note: Do not auto-relable fragments. We rely on per-test strict emission - # and the backfill step to surface missing tests explicitly. - PY - - - name: Backfill missing NL/T tests (fail placeholders) - if: always() - shell: bash - run: | - python3 - <<'PY' - from pathlib import Path - import xml.etree.ElementTree as ET - import re - - DESIRED = ["NL-0","NL-1","NL-2","NL-3","NL-4","T-A","T-B","T-C","T-D","T-E","T-F","T-G","T-H","T-I","T-J"] - seen = set() - def id_from_filename(p: Path): - n = p.name - m = re.match(r'NL(\d+)_results\.xml$', n, re.I) + txt = Path(frag).read_text(encoding='utf-8', errors='replace') + # Extract all testcase nodes from raw text + nodes = re.findall(r'', txt, flags=re.DOTALL) + for m in nodes: + try: + tcs.append(ET.fromstring(m)) + except Exception: + pass + + # Guard: keep only the first testcase from each fragment + if len(tcs) > 1: + tcs = tcs[:1] + + test_id = id_from_filename(frag) + + for tc in tcs: + current_name = tc.get('name') or '' + tid = test_id or id_from_system_out(tc) + # Enforce filename-derived ID as prefix; repair names if needed + if tid and not re.match(r'^\s*(NL-\d+|T-[A-Z])\b', current_name): + title = current_name.strip() + new_name = f'{tid} — {title}' if title else tid + tc.set('name', new_name) + elif tid and not re.match(rf'^\s*{re.escape(tid)}\b', current_name): + # Replace any wrong leading ID with the correct one + title = re.sub(r'^\s*(NL-\d+|T-[A-Z])\s*[—–:\-]\s*', '', current_name).strip() + new_name = f'{tid} — {title}' if title else tid + tc.set('name', new_name) + renamed += 1 + suite.append(tc) + added += 1 + + if added: + # Drop bootstrap placeholder and recompute counts + for tc in list(suite.findall('.//testcase')): + if (tc.get('name') or '') == 'NL-Suite.Bootstrap': + suite.remove(tc) + testcases = suite.findall('.//testcase') + failures_cnt = sum(1 for tc in testcases if (tc.find('failure') is not None or tc.find('error') is not None)) + suite.set('tests', str(len(testcases))) + suite.set('failures', str(failures_cnt)) + suite.set('errors', '0') + suite.set('skipped', '0') + tree.write(src, encoding='utf-8', xml_declaration=True) + print(f"Appended {added} testcase(s); renamed {renamed} to canonical NL/T names.") + PY + + # ---------- Markdown summary from JUnit ---------- + - name: Build markdown summary from JUnit + if: always() + shell: bash + run: | + python3 - <<'PY' + import xml.etree.ElementTree as ET + from pathlib import Path + import os, html, re + + def localname(tag: str) -> str: + return tag.rsplit('}', 1)[-1] if '}' in tag else tag + + src = Path(os.environ.get('JUNIT_OUT', 'reports/junit-nl-suite.xml')) + md_out = Path(os.environ.get('MD_OUT', 'reports/junit-nl-suite.md')) + md_out.parent.mkdir(parents=True, exist_ok=True) + + if not src.exists(): + md_out.write_text("# Unity NL/T Editing Suite Test Results\n\n(No JUnit found)\n", encoding='utf-8') + raise SystemExit(0) + + tree = ET.parse(src) + root = tree.getroot() + suite = root.find('./*') if localname(root.tag) == 'testsuites' else root + cases = [] if suite is None else list(suite.findall('.//testcase')) + + def id_from_case(tc): + n = (tc.get('name') or '') + m = re.match(r'\s*(NL-\d+|T-[A-Z])\b', n) if m: - return f"NL-{int(m.group(1))}" - m = re.match(r'T([A-J])_results\.xml$', n, re.I) - if m: - return f"T-{m.group(1).upper()}" + return m.group(1) + so = tc.find('system-out') + if so is not None and so.text: + m = re.search(r'\b(NL-\d+|T-[A-Z])\b', so.text) + if m: + return m.group(1) return None - for p in Path("reports").glob("*_results.xml"): - try: - r = ET.parse(p).getroot() - except Exception: - continue - # Count by filename id primarily; fall back to testcase name if needed - fid = id_from_filename(p) - if fid in DESIRED: - seen.add(fid) - continue - if r.tag == "testcase": - name = (r.get("name") or "").strip() - for d in DESIRED: - if name.startswith(d): - seen.add(d) - break - - Path("reports").mkdir(parents=True, exist_ok=True) - for d in DESIRED: - if d in seen: - continue - frag = Path(f"reports/{d}_results.xml") - tc = ET.Element("testcase", {"classname":"UnityMCP.NL-T", "name": d}) - fail = ET.SubElement(tc, "failure", {"message":"not produced"}) - fail.text = "The agent did not emit a fragment for this test." - ET.ElementTree(tc).write(frag, encoding="utf-8", xml_declaration=False) - print(f"backfill: {d}") - PY - - - name: "Debug: list testcase names" - if: always() - run: | - python3 - <<'PY' - from pathlib import Path - import xml.etree.ElementTree as ET - for p in sorted(Path('reports').glob('*_results.xml')): - try: - r = ET.parse(p).getroot() - if r.tag == 'testcase': - print(f"{p.name}: {(r.get('name') or '').strip()}") - except Exception: - pass - PY - - # ---------- Merge testcase fragments into JUnit ---------- - - name: Normalize/assemble JUnit in-place (single file) - if: always() - shell: bash - run: | - python3 - <<'PY' - from pathlib import Path - import xml.etree.ElementTree as ET - import re, os - - def localname(tag: str) -> str: - return tag.rsplit('}', 1)[-1] if '}' in tag else tag - - src = Path(os.environ.get('JUNIT_OUT', 'reports/junit-nl-suite.xml')) - if not src.exists(): - raise SystemExit(0) - - tree = ET.parse(src) - root = tree.getroot() - suite = root.find('./*') if localname(root.tag) == 'testsuites' else root - if suite is None: - raise SystemExit(0) - - def id_from_filename(p: Path): - n = p.name - m = re.match(r'NL(\d+)_results\.xml$', n, re.I) - if m: - return f"NL-{int(m.group(1))}" - m = re.match(r'T([A-J])_results\.xml$', n, re.I) - if m: - return f"T-{m.group(1).upper()}" - return None - - def id_from_system_out(tc): - so = tc.find('system-out') - if so is not None and so.text: - m = re.search(r'\b(NL-\d+|T-[A-Z])\b', so.text) - if m: - return m.group(1) - return None - - fragments = sorted(Path('reports').glob('*_results.xml')) - added = 0 - renamed = 0 - - for frag in fragments: - tcs = [] - try: - froot = ET.parse(frag).getroot() - if localname(froot.tag) == 'testcase': - tcs = [froot] - else: - tcs = list(froot.findall('.//testcase')) - except Exception: - txt = Path(frag).read_text(encoding='utf-8', errors='replace') - # Extract all testcase nodes from raw text - nodes = re.findall(r'', txt, flags=re.DOTALL) - for m in nodes: - try: - tcs.append(ET.fromstring(m)) - except Exception: - pass - - # Guard: keep only the first testcase from each fragment - if len(tcs) > 1: - tcs = tcs[:1] - - test_id = id_from_filename(frag) - - for tc in tcs: - current_name = tc.get('name') or '' - tid = test_id or id_from_system_out(tc) - # Enforce filename-derived ID as prefix; repair names if needed - if tid and not re.match(r'^\s*(NL-\d+|T-[A-Z])\b', current_name): - title = current_name.strip() - new_name = f'{tid} — {title}' if title else tid - tc.set('name', new_name) - elif tid and not re.match(rf'^\s*{re.escape(tid)}\b', current_name): - # Replace any wrong leading ID with the correct one - title = re.sub(r'^\s*(NL-\d+|T-[A-Z])\s*[—–:\-]\s*', '', current_name).strip() - new_name = f'{tid} — {title}' if title else tid - tc.set('name', new_name) - renamed += 1 - suite.append(tc) - added += 1 - - if added: - # Drop bootstrap placeholder and recompute counts - for tc in list(suite.findall('.//testcase')): - if (tc.get('name') or '') == 'NL-Suite.Bootstrap': - suite.remove(tc) - testcases = suite.findall('.//testcase') - failures_cnt = sum(1 for tc in testcases if (tc.find('failure') is not None or tc.find('error') is not None)) - suite.set('tests', str(len(testcases))) - suite.set('failures', str(failures_cnt)) - suite.set('errors', '0') - suite.set('skipped', '0') - tree.write(src, encoding='utf-8', xml_declaration=True) - print(f"Appended {added} testcase(s); renamed {renamed} to canonical NL/T names.") - PY - - # ---------- Markdown summary from JUnit ---------- - - name: Build markdown summary from JUnit - if: always() - shell: bash - run: | - python3 - <<'PY' - import xml.etree.ElementTree as ET - from pathlib import Path - import os, html, re - - def localname(tag: str) -> str: - return tag.rsplit('}', 1)[-1] if '}' in tag else tag - - src = Path(os.environ.get('JUNIT_OUT', 'reports/junit-nl-suite.xml')) - md_out = Path(os.environ.get('MD_OUT', 'reports/junit-nl-suite.md')) - md_out.parent.mkdir(parents=True, exist_ok=True) - - if not src.exists(): - md_out.write_text("# Unity NL/T Editing Suite Test Results\n\n(No JUnit found)\n", encoding='utf-8') - raise SystemExit(0) - - tree = ET.parse(src) - root = tree.getroot() - suite = root.find('./*') if localname(root.tag) == 'testsuites' else root - cases = [] if suite is None else list(suite.findall('.//testcase')) - - def id_from_case(tc): - n = (tc.get('name') or '') - m = re.match(r'\s*(NL-\d+|T-[A-Z])\b', n) - if m: - return m.group(1) - so = tc.find('system-out') - if so is not None and so.text: - m = re.search(r'\b(NL-\d+|T-[A-Z])\b', so.text) - if m: - return m.group(1) - return None - - id_status = {} - name_map = {} - for tc in cases: - tid = id_from_case(tc) - ok = (tc.find('failure') is None and tc.find('error') is None) - if tid and tid not in id_status: - id_status[tid] = ok - name_map[tid] = (tc.get('name') or tid) - - desired = ['NL-0','NL-1','NL-2','NL-3','NL-4','T-A','T-B','T-C','T-D','T-E','T-F','T-G','T-H','T-I','T-J'] - - total = len(cases) - failures = sum(1 for tc in cases if (tc.find('failure') is not None or tc.find('error') is not None)) - passed = total - failures - - lines = [] - lines += [ - '# Unity NL/T Editing Suite Test Results', - '', - f'Totals: {passed} passed, {failures} failed, {total} total', - '', - '## Test Checklist' - ] - for p in desired: - st = id_status.get(p, None) - lines.append(f"- [x] {p}" if st is True else (f"- [ ] {p} (fail)" if st is False else f"- [ ] {p} (not run)")) - lines.append('') - - lines.append('## Test Details') - - def order_key(n: str): - if n.startswith('NL-'): - try: - return (0, int(n.split('-')[1])) - except: - return (0, 999) - if n.startswith('T-') and len(n) > 2: - return (1, ord(n[2])) - return (2, n) - - MAX_CHARS = 2000 - seen = set() - for tid in sorted(id_status.keys(), key=order_key): - seen.add(tid) - tc = next((c for c in cases if (id_from_case(c) == tid)), None) - if not tc: - continue - title = name_map.get(tid, tid) - status_badge = "PASS" if id_status[tid] else "FAIL" - lines.append(f"### {title} — {status_badge}") - so = tc.find('system-out') - text = '' if so is None or so.text is None else html.unescape(so.text.replace('\r\n','\n')) - if text.strip(): - t = text.strip() - if len(t) > MAX_CHARS: - t = t[:MAX_CHARS] + "\n…(truncated)" - fence = '```' if '```' not in t else '````' - lines += [fence, t, fence] - else: - lines.append('(no system-out)') - node = tc.find('failure') or tc.find('error') - if node is not None: - msg = (node.get('message') or '').strip() - body = (node.text or '').strip() - if msg: - lines.append(f"- Message: {msg}") - if body: - lines.append(f"- Detail: {body.splitlines()[0][:500]}") - lines.append('') - - for tc in cases: - if id_from_case(tc) in seen: - continue - title = tc.get('name') or '(unnamed)' - status_badge = "PASS" if (tc.find('failure') is None and tc.find('error') is None) else "FAIL" - lines.append(f"### {title} — {status_badge}") - lines.append('(unmapped test id)') - lines.append('') - - md_out.write_text('\n'.join(lines), encoding='utf-8') - PY - - - name: "Debug: list report files" - if: always() - shell: bash - run: | - set -eux - ls -la reports || true - shopt -s nullglob - for f in reports/*.xml; do - echo "===== $f =====" - head -n 40 "$f" || true - done - - # ---------- Collect execution transcript (if present) ---------- - - name: Collect action execution transcript - if: always() - shell: bash - run: | - set -eux - if [ -f "$RUNNER_TEMP/claude-execution-output.json" ]; then - cp "$RUNNER_TEMP/claude-execution-output.json" reports/claude-execution-output.json - elif [ -f "/home/runner/work/_temp/claude-execution-output.json" ]; then - cp "/home/runner/work/_temp/claude-execution-output.json" reports/claude-execution-output.json - fi - - - name: Sanitize markdown (normalize newlines) - if: always() - run: | - set -eu - python3 - <<'PY' - from pathlib import Path - rp=Path('reports'); rp.mkdir(parents=True, exist_ok=True) - for p in rp.glob('*.md'): - b=p.read_bytes().replace(b'\x00', b'') - s=b.decode('utf-8','replace').replace('\r\n','\n') - p.write_text(s, encoding='utf-8', newline='\n') - PY - - - name: NL/T details -> Job Summary - if: always() - run: | - echo "## Unity NL/T Editing Suite — Summary" >> $GITHUB_STEP_SUMMARY - python3 - <<'PY' >> $GITHUB_STEP_SUMMARY - from pathlib import Path - p = Path('reports/junit-nl-suite.md') - if p.exists(): - text = p.read_bytes().decode('utf-8', 'replace') - MAX = 65000 - print(text[:MAX]) - if len(text) > MAX: - print("\n\n_…truncated; full report in artifacts._") - else: - print("_No markdown report found._") - PY - - - name: Fallback JUnit if missing - if: always() - run: | - set -eu - mkdir -p reports - if [ ! -f "$JUNIT_OUT" ]; then - printf '%s\n' \ - '' \ - '' \ - ' ' \ - ' ' \ - ' ' \ - '' \ - > "$JUNIT_OUT" - fi - - - name: Publish JUnit report - if: always() - uses: mikepenz/action-junit-report@v5 - with: - report_paths: '${{ env.JUNIT_OUT }}' - include_passed: true - detailed_summary: true - annotate_notice: true - require_tests: false - fail_on_parse_error: true - - - name: Upload artifacts (reports + fragments + transcript) - if: always() - uses: actions/upload-artifact@v4 - with: - name: claude-nl-suite-artifacts - path: | - ${{ env.JUNIT_OUT }} - ${{ env.MD_OUT }} - reports/*_results.xml - reports/claude-execution-output.json - retention-days: 7 - - # ---------- Always stop Unity ---------- - - name: Stop Unity - if: always() - run: | - docker logs --tail 400 unity-mcp | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/ig' || true - docker rm -f unity-mcp || true - - - name: Return Pro license (if used) - if: always() && steps.lic.outputs.use_ebl == 'true' && steps.lic.outputs.has_serial == 'true' - uses: game-ci/unity-return-license@v2 - continue-on-error: true - env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - \ No newline at end of file + id_status = {} + name_map = {} + for tc in cases: + tid = id_from_case(tc) + ok = (tc.find('failure') is None and tc.find('error') is None) + if tid and tid not in id_status: + id_status[tid] = ok + name_map[tid] = (tc.get('name') or tid) + + desired = ['NL-0','NL-1','NL-2','NL-3','NL-4','T-A','T-B','T-C','T-D','T-E','T-F','T-G','T-H','T-I','T-J'] + + total = len(cases) + failures = sum(1 for tc in cases if (tc.find('failure') is not None or tc.find('error') is not None)) + passed = total - failures + + lines = [] + lines += [ + '# Unity NL/T Editing Suite Test Results', + '', + f'Totals: {passed} passed, {failures} failed, {total} total', + '', + '## Test Checklist' + ] + for p in desired: + st = id_status.get(p, None) + lines.append(f"- [x] {p}" if st is True else (f"- [ ] {p} (fail)" if st is False else f"- [ ] {p} (not run)")) + lines.append('') + + lines.append('## Test Details') + + def order_key(n: str): + if n.startswith('NL-'): + try: + return (0, int(n.split('-')[1])) + except: + return (0, 999) + if n.startswith('T-') and len(n) > 2: + return (1, ord(n[2])) + return (2, n) + + MAX_CHARS = 2000 + seen = set() + for tid in sorted(id_status.keys(), key=order_key): + seen.add(tid) + tc = next((c for c in cases if (id_from_case(c) == tid)), None) + if not tc: + continue + title = name_map.get(tid, tid) + status_badge = "PASS" if id_status[tid] else "FAIL" + lines.append(f"### {title} — {status_badge}") + so = tc.find('system-out') + text = '' if so is None or so.text is None else html.unescape(so.text.replace('\r\n','\n')) + if text.strip(): + t = text.strip() + if len(t) > MAX_CHARS: + t = t[:MAX_CHARS] + "\n…(truncated)" + fence = '```' if '```' not in t else '````' + lines += [fence, t, fence] + else: + lines.append('(no system-out)') + node = tc.find('failure') or tc.find('error') + if node is not None: + msg = (node.get('message') or '').strip() + body = (node.text or '').strip() + if msg: + lines.append(f"- Message: {msg}") + if body: + lines.append(f"- Detail: {body.splitlines()[0][:500]}") + lines.append('') + + for tc in cases: + if id_from_case(tc) in seen: + continue + title = tc.get('name') or '(unnamed)' + status_badge = "PASS" if (tc.find('failure') is None and tc.find('error') is None) else "FAIL" + lines.append(f"### {title} — {status_badge}") + lines.append('(unmapped test id)') + lines.append('') + + md_out.write_text('\n'.join(lines), encoding='utf-8') + PY + + - name: "Debug: list report files" + if: always() + shell: bash + run: | + set -eux + ls -la reports || true + shopt -s nullglob + for f in reports/*.xml; do + echo "===== $f =====" + head -n 40 "$f" || true + done + + # ---------- Collect execution transcript (if present) ---------- + - name: Collect action execution transcript + if: always() + shell: bash + run: | + set -eux + if [ -f "$RUNNER_TEMP/claude-execution-output.json" ]; then + cp "$RUNNER_TEMP/claude-execution-output.json" reports/claude-execution-output.json + elif [ -f "/home/runner/work/_temp/claude-execution-output.json" ]; then + cp "/home/runner/work/_temp/claude-execution-output.json" reports/claude-execution-output.json + fi + + - name: Sanitize markdown (normalize newlines) + if: always() + run: | + set -eu + python3 - <<'PY' + from pathlib import Path + rp=Path('reports'); rp.mkdir(parents=True, exist_ok=True) + for p in rp.glob('*.md'): + b=p.read_bytes().replace(b'\x00', b'') + s=b.decode('utf-8','replace').replace('\r\n','\n') + p.write_text(s, encoding='utf-8', newline='\n') + PY + + - name: NL/T details -> Job Summary + if: always() + run: | + echo "## Unity NL/T Editing Suite — Summary" >> $GITHUB_STEP_SUMMARY + python3 - <<'PY' >> $GITHUB_STEP_SUMMARY + from pathlib import Path + p = Path('reports/junit-nl-suite.md') + if p.exists(): + text = p.read_bytes().decode('utf-8', 'replace') + MAX = 65000 + print(text[:MAX]) + if len(text) > MAX: + print("\n\n_…truncated; full report in artifacts._") + else: + print("_No markdown report found._") + PY + + - name: Fallback JUnit if missing + if: always() + run: | + set -eu + mkdir -p reports + if [ ! -f "$JUNIT_OUT" ]; then + printf '%s\n' \ + '' \ + '' \ + ' ' \ + ' ' \ + ' ' \ + '' \ + > "$JUNIT_OUT" + fi + + - name: Publish JUnit report + if: always() + uses: mikepenz/action-junit-report@v5 + with: + report_paths: "${{ env.JUNIT_OUT }}" + include_passed: true + detailed_summary: true + annotate_notice: true + require_tests: false + fail_on_parse_error: true + + - name: Upload artifacts (reports + fragments + transcript) + if: always() + uses: actions/upload-artifact@v4 + with: + name: claude-nl-suite-artifacts + path: | + ${{ env.JUNIT_OUT }} + ${{ env.MD_OUT }} + reports/*_results.xml + reports/claude-execution-output.json + retention-days: 7 + + # ---------- Always stop Unity ---------- + - name: Stop Unity + if: always() + run: | + docker logs --tail 400 unity-mcp | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/ig' || true + docker rm -f unity-mcp || true + + - name: Return Pro license (if used) + if: always() && steps.lic.outputs.use_ebl == 'true' && steps.lic.outputs.has_serial == 'true' + uses: game-ci/unity-return-license@v2 + continue-on-error: true + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} diff --git a/.github/workflows/github-repo-stats.yml b/.github/workflows/github-repo-stats.yml index fda0851b..fb130a1b 100644 --- a/.github/workflows/github-repo-stats.yml +++ b/.github/workflows/github-repo-stats.yml @@ -17,4 +17,3 @@ jobs: uses: jgehrcke/github-repo-stats@RELEASE with: ghtoken: ${{ secrets.ghrs_github_api_token }} - diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index e1dea5a2..0b230966 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -2,7 +2,7 @@ name: Unity Tests on: push: - branches: [ main ] + branches: [main] paths: - TestProjects/UnityMCPTests/** - UnityMcpBridge/Editor/** diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/Hello.cs b/TestProjects/UnityMCPTests/Assets/Scripts/Hello.cs index f7fd8f3b..1f083600 100644 --- a/TestProjects/UnityMCPTests/Assets/Scripts/Hello.cs +++ b/TestProjects/UnityMCPTests/Assets/Scripts/Hello.cs @@ -3,13 +3,8 @@ public class Hello : MonoBehaviour { - - // Use this for initialization void Start() { Debug.Log("Hello World"); } - - - } diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs b/TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs index 27fb9348..916a0f94 100644 --- a/TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs +++ b/TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs @@ -2035,5 +2035,3 @@ private void Pad0650() #endregion } - - diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs index b38e5188..b9e4a3b8 100644 --- a/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs +++ b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs @@ -6,7 +6,7 @@ public class CustomComponent : MonoBehaviour { [SerializeField] private string customText = "Hello from custom asmdef!"; - + [SerializeField] private float customFloat = 42.0f; @@ -15,4 +15,4 @@ void Start() Debug.Log($"CustomComponent started: {customText}, value: {customFloat}"); } } -} \ No newline at end of file +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs index 8354e3f0..e52c7d0b 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs @@ -17,7 +17,7 @@ public void SetUp() sampleProperties = new List { "maxReachDistance", - "maxHorizontalDistance", + "maxHorizontalDistance", "maxVerticalDistance", "moveSpeed", "healthPoints", @@ -33,7 +33,7 @@ public void SetUp() public void GetAllComponentProperties_ReturnsValidProperties_ForTransform() { var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform)); - + Assert.IsNotEmpty(properties, "Transform should have properties"); Assert.Contains("position", properties, "Transform should have position property"); Assert.Contains("rotation", properties, "Transform should have rotation property"); @@ -44,7 +44,7 @@ public void GetAllComponentProperties_ReturnsValidProperties_ForTransform() public void GetAllComponentProperties_ReturnsEmpty_ForNullType() { var properties = ComponentResolver.GetAllComponentProperties(null); - + Assert.IsEmpty(properties, "Null type should return empty list"); } @@ -52,7 +52,7 @@ public void GetAllComponentProperties_ReturnsEmpty_ForNullType() public void GetAIPropertySuggestions_ReturnsEmpty_ForNullInput() { var suggestions = ComponentResolver.GetAIPropertySuggestions(null, sampleProperties); - + Assert.IsEmpty(suggestions, "Null input should return no suggestions"); } @@ -60,7 +60,7 @@ public void GetAIPropertySuggestions_ReturnsEmpty_ForNullInput() public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyInput() { var suggestions = ComponentResolver.GetAIPropertySuggestions("", sampleProperties); - + Assert.IsEmpty(suggestions, "Empty input should return no suggestions"); } @@ -68,7 +68,7 @@ public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyInput() public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyPropertyList() { var suggestions = ComponentResolver.GetAIPropertySuggestions("test", new List()); - + Assert.IsEmpty(suggestions, "Empty property list should return no suggestions"); } @@ -76,7 +76,7 @@ public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyPropertyList() public void GetAIPropertySuggestions_FindsExactMatch_AfterCleaning() { var suggestions = ComponentResolver.GetAIPropertySuggestions("Max Reach Distance", sampleProperties); - + Assert.Contains("maxReachDistance", suggestions, "Should find exact match after cleaning spaces"); Assert.GreaterOrEqual(suggestions.Count, 1, "Should return at least one match for exact match"); } @@ -85,9 +85,9 @@ public void GetAIPropertySuggestions_FindsExactMatch_AfterCleaning() public void GetAIPropertySuggestions_FindsMultipleWordMatches() { var suggestions = ComponentResolver.GetAIPropertySuggestions("max distance", sampleProperties); - + Assert.Contains("maxReachDistance", suggestions, "Should match maxReachDistance"); - Assert.Contains("maxHorizontalDistance", suggestions, "Should match maxHorizontalDistance"); + Assert.Contains("maxHorizontalDistance", suggestions, "Should match maxHorizontalDistance"); Assert.Contains("maxVerticalDistance", suggestions, "Should match maxVerticalDistance"); } @@ -95,7 +95,7 @@ public void GetAIPropertySuggestions_FindsMultipleWordMatches() public void GetAIPropertySuggestions_FindsSimilarStrings_WithTypos() { var suggestions = ComponentResolver.GetAIPropertySuggestions("movespeed", sampleProperties); // missing capital S - + Assert.Contains("moveSpeed", suggestions, "Should find moveSpeed despite missing capital"); } @@ -103,7 +103,7 @@ public void GetAIPropertySuggestions_FindsSimilarStrings_WithTypos() public void GetAIPropertySuggestions_FindsSemanticMatches_ForCommonTerms() { var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", sampleProperties); - + // Note: Current algorithm might not find "mass" but should handle it gracefully Assert.IsNotNull(suggestions, "Should return valid suggestions list"); } @@ -113,7 +113,7 @@ public void GetAIPropertySuggestions_LimitsResults_ToReasonableNumber() { // Test with input that might match many properties var suggestions = ComponentResolver.GetAIPropertySuggestions("m", sampleProperties); - + Assert.LessOrEqual(suggestions.Count, 3, "Should limit suggestions to 3 or fewer"); } @@ -121,13 +121,13 @@ public void GetAIPropertySuggestions_LimitsResults_ToReasonableNumber() public void GetAIPropertySuggestions_CachesResults() { var input = "Max Reach Distance"; - + // First call var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties); - + // Second call should use cache (tested indirectly by ensuring consistency) var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties); - + Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be consistent"); CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should be identical"); } @@ -136,11 +136,11 @@ public void GetAIPropertySuggestions_CachesResults() public void GetAIPropertySuggestions_HandlesUnityNamingConventions() { var unityStyleProperties = new List { "isKinematic", "useGravity", "maxLinearVelocity" }; - + var suggestions1 = ComponentResolver.GetAIPropertySuggestions("is kinematic", unityStyleProperties); var suggestions2 = ComponentResolver.GetAIPropertySuggestions("use gravity", unityStyleProperties); var suggestions3 = ComponentResolver.GetAIPropertySuggestions("max linear velocity", unityStyleProperties); - + Assert.Contains("isKinematic", suggestions1, "Should handle 'is' prefix convention"); Assert.Contains("useGravity", suggestions2, "Should handle 'use' prefix convention"); Assert.Contains("maxLinearVelocity", suggestions3, "Should handle 'max' prefix convention"); @@ -151,7 +151,7 @@ public void GetAIPropertySuggestions_PrioritizesExactMatches() { var properties = new List { "speed", "moveSpeed", "maxSpeed", "speedMultiplier" }; var suggestions = ComponentResolver.GetAIPropertySuggestions("speed", properties); - + Assert.IsNotEmpty(suggestions, "Should find suggestions"); Assert.Contains("speed", suggestions, "Exact match should be included in results"); // Note: Implementation may or may not prioritize exact matches first @@ -162,7 +162,7 @@ public void GetAIPropertySuggestions_HandlesCaseInsensitive() { var suggestions1 = ComponentResolver.GetAIPropertySuggestions("MAXREACHDISTANCE", sampleProperties); var suggestions2 = ComponentResolver.GetAIPropertySuggestions("maxreachdistance", sampleProperties); - + Assert.Contains("maxReachDistance", suggestions1, "Should handle uppercase input"); Assert.Contains("maxReachDistance", suggestions2, "Should handle lowercase input"); } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs index 9b24456b..5ab03e80 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs @@ -12,7 +12,7 @@ public class ComponentResolverTests public void TryResolve_ReturnsTrue_ForBuiltInComponentShortName() { bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error); - + Assert.IsTrue(result, "Should resolve Transform component"); Assert.AreEqual(typeof(Transform), type, "Should return correct Transform type"); Assert.IsEmpty(error, "Should have no error message"); @@ -22,7 +22,7 @@ public void TryResolve_ReturnsTrue_ForBuiltInComponentShortName() public void TryResolve_ReturnsTrue_ForBuiltInComponentFullyQualifiedName() { bool result = ComponentResolver.TryResolve("UnityEngine.Rigidbody", out Type type, out string error); - + Assert.IsTrue(result, "Should resolve UnityEngine.Rigidbody component"); Assert.AreEqual(typeof(Rigidbody), type, "Should return correct Rigidbody type"); Assert.IsEmpty(error, "Should have no error message"); @@ -32,7 +32,7 @@ public void TryResolve_ReturnsTrue_ForBuiltInComponentFullyQualifiedName() public void TryResolve_ReturnsTrue_ForCustomComponentShortName() { bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error); - + Assert.IsTrue(result, "Should resolve CustomComponent"); Assert.IsNotNull(type, "Should return valid type"); Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name"); @@ -44,7 +44,7 @@ public void TryResolve_ReturnsTrue_ForCustomComponentShortName() public void TryResolve_ReturnsTrue_ForCustomComponentFullyQualifiedName() { bool result = ComponentResolver.TryResolve("TestNamespace.CustomComponent", out Type type, out string error); - + Assert.IsTrue(result, "Should resolve TestNamespace.CustomComponent"); Assert.IsNotNull(type, "Should return valid type"); Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name"); @@ -57,7 +57,7 @@ public void TryResolve_ReturnsTrue_ForCustomComponentFullyQualifiedName() public void TryResolve_ReturnsFalse_ForNonExistentComponent() { bool result = ComponentResolver.TryResolve("NonExistentComponent", out Type type, out string error); - + Assert.IsFalse(result, "Should not resolve non-existent component"); Assert.IsNull(type, "Should return null type"); Assert.IsNotEmpty(error, "Should have error message"); @@ -68,7 +68,7 @@ public void TryResolve_ReturnsFalse_ForNonExistentComponent() public void TryResolve_ReturnsFalse_ForEmptyString() { bool result = ComponentResolver.TryResolve("", out Type type, out string error); - + Assert.IsFalse(result, "Should not resolve empty string"); Assert.IsNull(type, "Should return null type"); Assert.IsNotEmpty(error, "Should have error message"); @@ -78,7 +78,7 @@ public void TryResolve_ReturnsFalse_ForEmptyString() public void TryResolve_ReturnsFalse_ForNullString() { bool result = ComponentResolver.TryResolve(null, out Type type, out string error); - + Assert.IsFalse(result, "Should not resolve null string"); Assert.IsNull(type, "Should return null type"); Assert.IsNotEmpty(error, "Should have error message"); @@ -90,10 +90,10 @@ public void TryResolve_CachesResolvedTypes() { // First call bool result1 = ComponentResolver.TryResolve("Transform", out Type type1, out string error1); - + // Second call should use cache bool result2 = ComponentResolver.TryResolve("Transform", out Type type2, out string error2); - + Assert.IsTrue(result1, "First call should succeed"); Assert.IsTrue(result2, "Second call should succeed"); Assert.AreSame(type1, type2, "Should return same type instance (cached)"); @@ -106,27 +106,27 @@ public void TryResolve_PrefersPlayerAssemblies() { // Test that custom user scripts (in Player assemblies) are found bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error); - + Assert.IsTrue(result, "Should resolve user script from Player assembly"); Assert.IsNotNull(type, "Should return valid type"); - + // Verify it's not from an Editor assembly by checking the assembly name string assemblyName = type.Assembly.GetName().Name; - Assert.That(assemblyName, Does.Not.Contain("Editor"), + Assert.That(assemblyName, Does.Not.Contain("Editor"), "User script should come from Player assembly, not Editor assembly"); - + // Verify it's from the TestAsmdef assembly (which is a Player assembly) - Assert.AreEqual("TestAsmdef", assemblyName, + Assert.AreEqual("TestAsmdef", assemblyName, "CustomComponent should be resolved from TestAsmdef assembly"); } - [Test] + [Test] public void TryResolve_HandlesDuplicateNames_WithAmbiguityError() { // This test would need duplicate component names to be meaningful // For now, test with a built-in component that should not have duplicates bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error); - + Assert.IsTrue(result, "Transform should resolve uniquely"); Assert.AreEqual(typeof(Transform), type, "Should return correct type"); Assert.IsEmpty(error, "Should have no ambiguity error"); @@ -136,11 +136,11 @@ public void TryResolve_HandlesDuplicateNames_WithAmbiguityError() public void ResolvedType_IsValidComponent() { bool result = ComponentResolver.TryResolve("Rigidbody", out Type type, out string error); - + Assert.IsTrue(result, "Should resolve Rigidbody"); Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Resolved type should be assignable from Component"); - Assert.IsTrue(typeof(MonoBehaviour).IsAssignableFrom(type) || + Assert.IsTrue(typeof(MonoBehaviour).IsAssignableFrom(type) || typeof(Component).IsAssignableFrom(type), "Should be a valid Unity component"); } } -} \ No newline at end of file +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs index 34138999..536fb681 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs @@ -12,7 +12,7 @@ namespace MCPForUnityTests.Editor.Tools public class ManageGameObjectTests { private GameObject testGameObject; - + [SetUp] public void SetUp() { @@ -20,7 +20,7 @@ public void SetUp() testGameObject = new GameObject("TestObject"); } - [TearDown] + [TearDown] public void TearDown() { // Clean up test GameObject @@ -34,17 +34,17 @@ public void TearDown() public void HandleCommand_ReturnsError_ForNullParams() { var result = ManageGameObject.HandleCommand(null); - + Assert.IsNotNull(result, "Should return a result object"); // Note: Actual error checking would need access to Response structure } - [Test] + [Test] public void HandleCommand_ReturnsError_ForEmptyParams() { var emptyParams = new JObject(); var result = ManageGameObject.HandleCommand(emptyParams); - + Assert.IsNotNull(result, "Should return a result object for empty params"); } @@ -56,11 +56,11 @@ public void HandleCommand_ProcessesValidCreateAction() ["action"] = "create", ["name"] = "TestCreateObject" }; - + var result = ManageGameObject.HandleCommand(createParams); - + Assert.IsNotNull(result, "Should return a result for valid create action"); - + // Clean up - find and destroy the created object var createdObject = GameObject.Find("TestCreateObject"); if (createdObject != null) @@ -74,7 +74,7 @@ public void ComponentResolver_Integration_WorksWithRealComponents() { // Test that our ComponentResolver works with actual Unity components var transformResult = ComponentResolver.TryResolve("Transform", out Type transformType, out string error); - + Assert.IsTrue(transformResult, "Should resolve Transform component"); Assert.AreEqual(typeof(Transform), transformType, "Should return correct Transform type"); Assert.IsEmpty(error, "Should have no error for valid component"); @@ -86,7 +86,7 @@ public void ComponentResolver_Integration_WorksWithBuiltInComponents() var components = new[] { ("Rigidbody", typeof(Rigidbody)), - ("Collider", typeof(Collider)), + ("Collider", typeof(Collider)), ("Renderer", typeof(Renderer)), ("Camera", typeof(Camera)), ("Light", typeof(Light)) @@ -95,11 +95,11 @@ public void ComponentResolver_Integration_WorksWithBuiltInComponents() foreach (var (componentName, expectedType) in components) { var result = ComponentResolver.TryResolve(componentName, out Type actualType, out string error); - + // Some components might not resolve (abstract classes), but the method should handle gracefully if (result) { - Assert.IsTrue(expectedType.IsAssignableFrom(actualType), + Assert.IsTrue(expectedType.IsAssignableFrom(actualType), $"{componentName} should resolve to assignable type"); } else @@ -114,13 +114,13 @@ public void PropertyMatching_Integration_WorksWithRealGameObject() { // Add a Rigidbody to test real property matching var rigidbody = testGameObject.AddComponent(); - + var properties = ComponentResolver.GetAllComponentProperties(typeof(Rigidbody)); - + Assert.IsNotEmpty(properties, "Rigidbody should have properties"); Assert.Contains("mass", properties, "Rigidbody should have mass property"); Assert.Contains("useGravity", properties, "Rigidbody should have useGravity property"); - + // Test AI suggestions var suggestions = ComponentResolver.GetAIPropertySuggestions("Use Gravity", properties); Assert.Contains("useGravity", suggestions, "Should suggest useGravity for 'Use Gravity'"); @@ -130,18 +130,18 @@ public void PropertyMatching_Integration_WorksWithRealGameObject() public void PropertyMatching_HandlesMonoBehaviourProperties() { var properties = ComponentResolver.GetAllComponentProperties(typeof(MonoBehaviour)); - + Assert.IsNotEmpty(properties, "MonoBehaviour should have properties"); Assert.Contains("enabled", properties, "MonoBehaviour should have enabled property"); Assert.Contains("name", properties, "MonoBehaviour should have name property"); Assert.Contains("tag", properties, "MonoBehaviour should have tag property"); } - [Test] + [Test] public void PropertyMatching_HandlesCaseVariations() { var testProperties = new List { "maxReachDistance", "playerHealth", "movementSpeed" }; - + var testCases = new[] { ("max reach distance", "maxReachDistance"), @@ -164,10 +164,10 @@ public void ErrorHandling_ReturnsHelpfulMessages() // This test verifies that error messages are helpful and contain suggestions var testProperties = new List { "mass", "velocity", "drag", "useGravity" }; var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", testProperties); - + // Even if no perfect match, should return valid list Assert.IsNotNull(suggestions, "Should return valid suggestions list"); - + // Test with completely invalid input var badSuggestions = ComponentResolver.GetAIPropertySuggestions("xyz123invalid", testProperties); Assert.IsNotNull(badSuggestions, "Should handle invalid input gracefully"); @@ -178,20 +178,20 @@ public void PerformanceTest_CachingWorks() { var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform)); var input = "Test Property Name"; - + // First call - populate cache var startTime = System.DateTime.UtcNow; var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, properties); var firstCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds; - + // Second call - should use cache startTime = System.DateTime.UtcNow; var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, properties); var secondCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds; - + Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be identical"); CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should match exactly"); - + // Second call should be faster (though this test might be flaky) Assert.LessOrEqual(secondCallTime, firstCallTime * 2, "Cached call should not be significantly slower"); } @@ -202,13 +202,13 @@ public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes() // Arrange - add Transform and Rigidbody components to test with var transform = testGameObject.transform; var rigidbody = testGameObject.AddComponent(); - + // Create a params object with mixed valid and invalid properties var setPropertiesParams = new JObject { ["action"] = "modify", ["target"] = testGameObject.name, - ["search_method"] = "by_name", + ["search_method"] = "by_name", ["componentProperties"] = new JObject { ["Transform"] = new JObject @@ -217,7 +217,7 @@ public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes() ["rotatoin"] = new JObject { ["x"] = 0.0f, ["y"] = 90.0f, ["z"] = 0.0f }, // Invalid (typo - should be rotation) ["localScale"] = new JObject { ["x"] = 2.0f, ["y"] = 2.0f, ["z"] = 2.0f } // Valid }, - ["Rigidbody"] = new JObject + ["Rigidbody"] = new JObject { ["mass"] = 5.0f, // Valid ["invalidProp"] = "test", // Invalid - doesn't exist @@ -231,7 +231,7 @@ public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes() var originalLocalScale = transform.localScale; var originalMass = rigidbody.mass; var originalUseGravity = rigidbody.useGravity; - + Debug.Log($"BEFORE TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}"); // Expect the warning logs from the invalid properties @@ -240,13 +240,13 @@ public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes() // Act var result = ManageGameObject.HandleCommand(setPropertiesParams); - + Debug.Log($"AFTER TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}"); Debug.Log($"AFTER TEST - LocalPosition: {transform.localPosition}"); Debug.Log($"AFTER TEST - LocalScale: {transform.localScale}"); // Assert - verify that valid properties were set despite invalid ones - Assert.AreEqual(new Vector3(1.0f, 2.0f, 3.0f), transform.localPosition, + Assert.AreEqual(new Vector3(1.0f, 2.0f, 3.0f), transform.localPosition, "Valid localPosition should be set even with other invalid properties"); Assert.AreEqual(new Vector3(2.0f, 2.0f, 2.0f), transform.localScale, "Valid localScale should be set even with other invalid properties"); @@ -257,7 +257,7 @@ public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes() // Verify the result indicates errors (since we had invalid properties) Assert.IsNotNull(result, "Should return a result object"); - + // The collect-and-continue behavior means we should get an error response // that contains info about the failed properties, but valid ones were still applied // This proves the collect-and-continue behavior is working @@ -288,16 +288,16 @@ public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes() Assert.IsTrue(foundInvalidProp, "errors should mention the 'invalidProp' property"); } - [Test] + [Test] public void SetComponentProperties_ContinuesAfterException() { // Arrange - create scenario that might cause exceptions var rigidbody = testGameObject.AddComponent(); - + // Set initial values that we'll change rigidbody.mass = 1.0f; rigidbody.useGravity = true; - + var setPropertiesParams = new JObject { ["action"] = "modify", @@ -329,7 +329,7 @@ public void SetComponentProperties_ContinuesAfterException() "UseGravity should be set even if previous property caused exception"); Assert.IsNotNull(result, "Should return a result even with exceptions"); - + // The key test: processing continued after the exception and set useGravity // This proves the collect-and-continue behavior works even with exceptions @@ -356,4 +356,4 @@ public void SetComponentProperties_ContinuesAfterException() Assert.IsTrue(foundVelocityError, "errors should include a message referencing 'velocity'"); } } -} \ No newline at end of file +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs index dd379372..37f2f268 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptValidationTests.cs @@ -29,7 +29,7 @@ public void HandleCommand_InvalidAction_ReturnsError() ["name"] = "TestScript", ["path"] = "Assets/Scripts" }; - + var result = ManageScript.HandleCommand(paramsObj); Assert.IsNotNull(result, "Should return error result for invalid action"); } @@ -38,7 +38,7 @@ public void HandleCommand_InvalidAction_ReturnsError() public void CheckBalancedDelimiters_ValidCode_ReturnsTrue() { string validCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n void Start()\n {\n Debug.Log(\"test\");\n }\n}"; - + bool result = CallCheckBalancedDelimiters(validCode, out int line, out char expected); Assert.IsTrue(result, "Valid C# code should pass balance check"); } @@ -47,7 +47,7 @@ public void CheckBalancedDelimiters_ValidCode_ReturnsTrue() public void CheckBalancedDelimiters_UnbalancedBraces_ReturnsFalse() { string unbalancedCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n void Start()\n {\n Debug.Log(\"test\");\n // Missing closing brace"; - + bool result = CallCheckBalancedDelimiters(unbalancedCode, out int line, out char expected); Assert.IsFalse(result, "Unbalanced code should fail balance check"); } @@ -56,16 +56,16 @@ public void CheckBalancedDelimiters_UnbalancedBraces_ReturnsFalse() public void CheckBalancedDelimiters_StringWithBraces_ReturnsTrue() { string codeWithStringBraces = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n public string json = \"{key: value}\";\n void Start() { Debug.Log(json); }\n}"; - + bool result = CallCheckBalancedDelimiters(codeWithStringBraces, out int line, out char expected); Assert.IsTrue(result, "Code with braces in strings should pass balance check"); } - [Test] + [Test] public void CheckScopedBalance_ValidCode_ReturnsTrue() { string validCode = "{ Debug.Log(\"test\"); }"; - + bool result = CallCheckScopedBalance(validCode, 0, validCode.Length); Assert.IsTrue(result, "Valid scoped code should pass balance check"); } @@ -75,9 +75,9 @@ public void CheckScopedBalance_ShouldTolerateOuterContext_ReturnsTrue() { // This simulates a snippet extracted from a larger context string contextSnippet = " Debug.Log(\"inside method\");\n} // This closing brace is from outer context"; - + bool result = CallCheckScopedBalance(contextSnippet, 0, contextSnippet.Length); - + // Scoped balance should tolerate some imbalance from outer context Assert.IsTrue(result, "Scoped balance should tolerate outer context imbalance"); } @@ -87,11 +87,11 @@ public void TicTacToe3D_ValidationScenario_DoesNotCrash() { // Test the scenario that was causing issues without file I/O string ticTacToeCode = "using UnityEngine;\n\npublic class TicTacToe3D : MonoBehaviour\n{\n public string gameState = \"active\";\n void Start() { Debug.Log(\"Game started\"); }\n public void MakeMove(int position) { if (gameState == \"active\") Debug.Log($\"Move {position}\"); }\n}"; - + // Test that the validation methods don't crash on this code bool balanceResult = CallCheckBalancedDelimiters(ticTacToeCode, out int line, out char expected); bool scopedResult = CallCheckScopedBalance(ticTacToeCode, 0, ticTacToeCode.Length); - + Assert.IsTrue(balanceResult, "TicTacToe3D code should pass balance validation"); Assert.IsTrue(scopedResult, "TicTacToe3D code should pass scoped balance validation"); } @@ -101,12 +101,12 @@ private bool CallCheckBalancedDelimiters(string contents, out int line, out char { line = 0; expected = ' '; - + try { - var method = typeof(ManageScript).GetMethod("CheckBalancedDelimiters", + var method = typeof(ManageScript).GetMethod("CheckBalancedDelimiters", BindingFlags.NonPublic | BindingFlags.Static); - + if (method != null) { var parameters = new object[] { contents, line, expected }; @@ -120,7 +120,7 @@ private bool CallCheckBalancedDelimiters(string contents, out int line, out char { Debug.LogWarning($"Could not test CheckBalancedDelimiters directly: {ex.Message}"); } - + // Fallback: basic structural check return BasicBalanceCheck(contents); } @@ -129,9 +129,9 @@ private bool CallCheckScopedBalance(string text, int start, int end) { try { - var method = typeof(ManageScript).GetMethod("CheckScopedBalance", + var method = typeof(ManageScript).GetMethod("CheckScopedBalance", BindingFlags.NonPublic | BindingFlags.Static); - + if (method != null) { return (bool)method.Invoke(null, new object[] { text, start, end }); @@ -141,7 +141,7 @@ private bool CallCheckScopedBalance(string text, int start, int end) { Debug.LogWarning($"Could not test CheckScopedBalance directly: {ex.Message}"); } - + return true; // Default to passing if we can't test the actual method } @@ -151,32 +151,32 @@ private bool BasicBalanceCheck(string contents) int braceCount = 0; bool inString = false; bool escaped = false; - + for (int i = 0; i < contents.Length; i++) { char c = contents[i]; - + if (escaped) { escaped = false; continue; } - + if (inString) { if (c == '\\') escaped = true; else if (c == '"') inString = false; continue; } - + if (c == '"') inString = true; else if (c == '{') braceCount++; else if (c == '}') braceCount--; - + if (braceCount < 0) return false; } - + return braceCount == 0; } } -} \ No newline at end of file +} diff --git a/UnityMcpBridge/Editor/AssemblyInfo.cs b/UnityMcpBridge/Editor/AssemblyInfo.cs index 30a86a36..bae75b67 100644 --- a/UnityMcpBridge/Editor/AssemblyInfo.cs +++ b/UnityMcpBridge/Editor/AssemblyInfo.cs @@ -1,3 +1,3 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")] \ No newline at end of file +[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")] diff --git a/UnityMcpBridge/Editor/Data/DefaultServerConfig.cs b/UnityMcpBridge/Editor/Data/DefaultServerConfig.cs index c7b0c9f6..59cced75 100644 --- a/UnityMcpBridge/Editor/Data/DefaultServerConfig.cs +++ b/UnityMcpBridge/Editor/Data/DefaultServerConfig.cs @@ -15,4 +15,3 @@ public class DefaultServerConfig : ServerConfig public new float retryDelay = 1.0f; } } - diff --git a/UnityMcpBridge/Editor/External/Tommy.cs b/UnityMcpBridge/Editor/External/Tommy.cs index 95399e40..22e83b81 100644 --- a/UnityMcpBridge/Editor/External/Tommy.cs +++ b/UnityMcpBridge/Editor/External/Tommy.cs @@ -121,19 +121,19 @@ public virtual void AddRange(IEnumerable nodes) #region Native type to TOML cast - public static implicit operator TomlNode(string value) => new TomlString {Value = value}; + public static implicit operator TomlNode(string value) => new TomlString { Value = value }; - public static implicit operator TomlNode(bool value) => new TomlBoolean {Value = value}; + public static implicit operator TomlNode(bool value) => new TomlBoolean { Value = value }; - public static implicit operator TomlNode(long value) => new TomlInteger {Value = value}; + public static implicit operator TomlNode(long value) => new TomlInteger { Value = value }; - public static implicit operator TomlNode(float value) => new TomlFloat {Value = value}; + public static implicit operator TomlNode(float value) => new TomlFloat { Value = value }; - public static implicit operator TomlNode(double value) => new TomlFloat {Value = value}; + public static implicit operator TomlNode(double value) => new TomlFloat { Value = value }; - public static implicit operator TomlNode(DateTime value) => new TomlDateTimeLocal {Value = value}; + public static implicit operator TomlNode(DateTime value) => new TomlDateTimeLocal { Value = value }; - public static implicit operator TomlNode(DateTimeOffset value) => new TomlDateTimeOffset {Value = value}; + public static implicit operator TomlNode(DateTimeOffset value) => new TomlDateTimeOffset { Value = value }; public static implicit operator TomlNode(TomlNode[] nodes) { @@ -148,11 +148,11 @@ public static implicit operator TomlNode(TomlNode[] nodes) public static implicit operator string(TomlNode value) => value.ToString(); - public static implicit operator int(TomlNode value) => (int) value.AsInteger.Value; + public static implicit operator int(TomlNode value) => (int)value.AsInteger.Value; public static implicit operator long(TomlNode value) => value.AsInteger.Value; - public static implicit operator float(TomlNode value) => (float) value.AsFloat.Value; + public static implicit operator float(TomlNode value) => (float)value.AsFloat.Value; public static implicit operator double(TomlNode value) => value.AsFloat.Value; @@ -212,7 +212,7 @@ public enum Base public override string ToInlineToml() => IntegerBase != Base.Decimal - ? $"0{TomlSyntax.BaseIdentifiers[(int) IntegerBase]}{Convert.ToString(Value, (int) IntegerBase)}" + ? $"0{TomlSyntax.BaseIdentifiers[(int)IntegerBase]}{Convert.ToString(Value, (int)IntegerBase)}" : Value.ToString(CultureInfo.InvariantCulture); } @@ -232,10 +232,10 @@ public class TomlFloat : TomlNode, IFormattable public override string ToInlineToml() => Value switch { - var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE, + var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE, var v when double.IsPositiveInfinity(v) => TomlSyntax.INF_VALUE, var v when double.IsNegativeInfinity(v) => TomlSyntax.NEG_INF_VALUE, - var v => v.ToString("G", CultureInfo.InvariantCulture).ToLowerInvariant() + var v => v.ToString("G", CultureInfo.InvariantCulture).ToLowerInvariant() }; } @@ -286,7 +286,7 @@ public enum DateTimeStyle Time, DateTime } - + public override bool IsDateTimeLocal { get; } = true; public DateTimeStyle Style { get; set; } = DateTimeStyle.DateTime; public DateTime Value { get; set; } @@ -303,7 +303,7 @@ public override string ToInlineToml() => { DateTimeStyle.Date => Value.ToString(TomlSyntax.LocalDateFormat), DateTimeStyle.Time => Value.ToString(TomlSyntax.RFC3339LocalTimeFormats[SecondsPrecision]), - var _ => Value.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision]) + var _ => Value.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision]) }; } @@ -422,12 +422,12 @@ public class TomlTable : TomlNode { private Dictionary children; internal bool isImplicit; - + public override bool HasValue { get; } = false; public override bool IsTable { get; } = true; public bool IsInline { get; set; } public Dictionary RawTable => children ??= new Dictionary(); - + public override TomlNode this[string key] { get @@ -478,7 +478,7 @@ private LinkedList> CollectCollapsedItems(string { var node = keyValuePair.Value; var key = keyValuePair.Key.AsKey(); - + if (node is TomlTable tbl) { var subnodes = tbl.CollectCollapsedItems($"{prefix}{key}.", level + 1, normalizeOrder); @@ -493,7 +493,7 @@ private LinkedList> CollectCollapsedItems(string else if (node.CollapseLevel == level) nodes.AddLast(new KeyValuePair($"{prefix}{key}", node)); } - + if (normalizeOrder) foreach (var kv in postNodes) nodes.AddLast(kv); @@ -513,11 +513,11 @@ internal void WriteTo(TextWriter tw, string name, bool writeSectionName) } var collapsedItems = CollectCollapsedItems(); - + if (collapsedItems.Count == 0) return; - var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable {IsInline: false} or TomlArray {IsTableArray: true}); + var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } or TomlArray { IsTableArray: true }); Comment?.AsComment(tw); @@ -539,7 +539,7 @@ internal void WriteTo(TextWriter tw, string name, bool writeSectionName) foreach (var collapsedItem in collapsedItems) { var key = collapsedItem.Key; - if (collapsedItem.Value is TomlArray {IsTableArray: true} or TomlTable {IsInline: false}) + if (collapsedItem.Value is TomlArray { IsTableArray: true } or TomlTable { IsInline: false }) { if (!first) tw.WriteLine(); first = false; @@ -547,13 +547,13 @@ internal void WriteTo(TextWriter tw, string name, bool writeSectionName) continue; } first = false; - + collapsedItem.Value.Comment?.AsComment(tw); tw.Write(key); tw.Write(' '); tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR); tw.Write(' '); - + collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}"); } } @@ -660,7 +660,7 @@ public TomlTable Parse() int currentChar; while ((currentChar = reader.Peek()) >= 0) { - var c = (char) currentChar; + var c = (char)currentChar; if (currentState == ParseState.None) { @@ -771,7 +771,7 @@ public TomlTable Parse() // Consume the ending bracket so we can peek the next character ConsumeChar(); var nextChar = reader.Peek(); - if (nextChar < 0 || (char) nextChar != TomlSyntax.TABLE_END_SYMBOL) + if (nextChar < 0 || (char)nextChar != TomlSyntax.TABLE_END_SYMBOL) { AddError($"Array table {".".Join(keyParts)} has only one closing bracket."); keyParts.Clear(); @@ -837,7 +837,7 @@ public TomlTable Parse() AddError($"Unexpected character \"{c}\" at the end of the line."); } - consume_character: + consume_character: reader.Read(); col++; } @@ -858,7 +858,7 @@ private bool AddError(string message, bool skipLine = true) if (skipLine) { reader.ReadLine(); - AdvanceLine(1); + AdvanceLine(1); } currentState = ParseState.None; return false; @@ -892,7 +892,7 @@ private TomlNode ReadKeyValuePair(List keyParts) int cur; while ((cur = reader.Peek()) >= 0) { - var c = (char) cur; + var c = (char)cur; if (TomlSyntax.IsQuoted(c) || TomlSyntax.IsBareKey(c)) { @@ -941,7 +941,7 @@ private TomlNode ReadValue(bool skipNewlines = false) int cur; while ((cur = reader.Peek()) >= 0) { - var c = (char) cur; + var c = (char)cur; if (TomlSyntax.IsWhiteSpace(c)) { @@ -982,7 +982,7 @@ private TomlNode ReadValue(bool skipNewlines = false) if (value is null) return null; - + return new TomlString { Value = value, @@ -994,8 +994,8 @@ private TomlNode ReadValue(bool skipNewlines = false) return c switch { TomlSyntax.INLINE_TABLE_START_SYMBOL => ReadInlineTable(), - TomlSyntax.ARRAY_START_SYMBOL => ReadArray(), - var _ => ReadTomlValue() + TomlSyntax.ARRAY_START_SYMBOL => ReadArray(), + var _ => ReadTomlValue() }; } @@ -1023,7 +1023,7 @@ private bool ReadKeyName(ref List parts, char until) int cur; while ((cur = reader.Peek()) >= 0) { - var c = (char) cur; + var c = (char)cur; // Reached the final character if (c == until) break; @@ -1062,7 +1062,7 @@ private bool ReadKeyName(ref List parts, char until) // Consume the quote character and read the key name col++; - buffer.Append(ReadQuotedValueSingleLine((char) reader.Read())); + buffer.Append(ReadQuotedValueSingleLine((char)reader.Read())); quoted = true; continue; } @@ -1076,7 +1076,7 @@ private bool ReadKeyName(ref List parts, char until) // If we see an invalid symbol, let the next parser handle it break; - consume_character: + consume_character: reader.Read(); col++; } @@ -1107,7 +1107,7 @@ private string ReadRawValue() int cur; while ((cur = reader.Peek()) >= 0) { - var c = (char) cur; + var c = (char)cur; if (c == TomlSyntax.COMMENT_SYMBOL || TomlSyntax.IsNewLine(c) || TomlSyntax.IsValueSeparator(c)) break; result.Append(c); ConsumeChar(); @@ -1134,9 +1134,9 @@ private TomlNode ReadTomlValue() TomlNode node = value switch { var v when TomlSyntax.IsBoolean(v) => bool.Parse(v), - var v when TomlSyntax.IsNaN(v) => double.NaN, - var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity, - var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity, + var v when TomlSyntax.IsNaN(v) => double.NaN, + var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity, + var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity, var v when TomlSyntax.IsInteger(v) => long.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), CultureInfo.InvariantCulture), var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), @@ -1144,7 +1144,7 @@ var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_ var v when TomlSyntax.IsIntegerWithBase(v, out var numberBase) => new TomlInteger { Value = Convert.ToInt64(value.Substring(2).RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), numberBase), - IntegerBase = (TomlInteger.Base) numberBase + IntegerBase = (TomlInteger.Base)numberBase }, var _ => null }; @@ -1187,7 +1187,7 @@ var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_ Style = TomlDateTimeLocal.DateTimeStyle.Time, SecondsPrecision = precision }; - + if (StringUtils.TryParseDateTime(value, TomlSyntax.RFC3339Formats, DateTimeStyles.None, @@ -1223,7 +1223,7 @@ private TomlArray ReadArray() int cur; while ((cur = reader.Peek()) >= 0) { - var c = (char) cur; + var c = (char)cur; if (c == TomlSyntax.ARRAY_END_SYMBOL) { @@ -1274,7 +1274,7 @@ private TomlArray ReadArray() expectValue = false; continue; - consume_character: + consume_character: ConsumeChar(); } @@ -1293,14 +1293,14 @@ private TomlArray ReadArray() private TomlNode ReadInlineTable() { ConsumeChar(); - var result = new TomlTable {IsInline = true}; + var result = new TomlTable { IsInline = true }; TomlNode currentValue = null; var separator = false; var keyParts = new List(); int cur; while ((cur = reader.Peek()) >= 0) { - var c = (char) cur; + var c = (char)cur; if (c == TomlSyntax.INLINE_TABLE_END_SYMBOL) { @@ -1343,7 +1343,7 @@ private TomlNode ReadInlineTable() currentValue = ReadKeyValuePair(keyParts); continue; - consume_character: + consume_character: ConsumeChar(); } @@ -1352,7 +1352,7 @@ private TomlNode ReadInlineTable() AddError("Trailing commas are not allowed in inline tables."); return null; } - + if (currentValue != null && !InsertNode(currentValue, result, keyParts)) return null; @@ -1394,15 +1394,15 @@ private bool IsTripleQuote(char quote, out char excess) return AddError("Unexpected end of file!"); } - if ((char) cur != quote) + if ((char)cur != quote) { excess = '\0'; return false; } // Consume the second quote - excess = (char) ConsumeChar(); - if ((cur = reader.Peek()) < 0 || (char) cur != quote) return false; + excess = (char)ConsumeChar(); + if ((cur = reader.Peek()) < 0 || (char)cur != quote) return false; // Consume the final quote ConsumeChar(); @@ -1420,7 +1420,7 @@ private bool ProcessQuotedValueCharacter(char quote, ref bool escaped) { if (TomlSyntax.MustBeEscaped(c)) - return AddError($"The character U+{(int) c:X8} must be escaped in a string!"); + return AddError($"The character U+{(int)c:X8} must be escaped in a string!"); if (escaped) { @@ -1487,7 +1487,7 @@ private string ReadQuotedValueSingleLine(char quote, char initialData = '\0') { // Consume the character col++; - var c = (char) cur; + var c = (char)cur; readDone = ProcessQuotedValueCharacter(quote, isNonLiteral, c, sb, ref escaped); if (readDone) { @@ -1529,10 +1529,10 @@ private string ReadQuotedValueMultiLine(char quote) int cur; while ((cur = ConsumeChar()) >= 0) { - var c = (char) cur; + var c = (char)cur; if (TomlSyntax.MustBeEscaped(c, true)) { - AddError($"The character U+{(int) c:X8} must be escaped!"); + AddError($"The character U+{(int)c:X8} must be escaped!"); return null; } // Trim the first newline @@ -1582,7 +1582,7 @@ private string ReadQuotedValueMultiLine(char quote) if (isBasic && c == TomlSyntax.ESCAPE_SYMBOL) { var next = reader.Peek(); - var nc = (char) next; + var nc = (char)next; if (next >= 0) { // ...and the next char is empty space, we must skip all whitespaces @@ -1614,7 +1614,7 @@ private string ReadQuotedValueMultiLine(char quote) quotesEncountered = 0; while ((cur = reader.Peek()) >= 0) { - var c = (char) cur; + var c = (char)cur; if (c == quote && ++quotesEncountered < 3) { sb.Append(c); @@ -1677,7 +1677,7 @@ private TomlTable CreateTable(TomlNode root, IList path, bool arrayTable { if (node.IsArray && arrayTable) { - var arr = (TomlArray) node; + var arr = (TomlArray)node; if (!arr.IsTableArray) { @@ -1695,7 +1695,7 @@ private TomlTable CreateTable(TomlNode root, IList path, bool arrayTable latestNode = arr[arr.ChildrenCount - 1]; continue; } - + if (node is TomlTable { IsInline: true }) { AddError($"Cannot create table {".".Join(path)} because it will edit an immutable table."); @@ -1751,13 +1751,13 @@ private TomlTable CreateTable(TomlNode root, IList path, bool arrayTable latestNode = node; } - var result = (TomlTable) latestNode; + var result = (TomlTable)latestNode; result.isImplicit = false; return result; } #endregion - + #region Misc parsing private string ParseComment() @@ -1779,7 +1779,7 @@ public static class TOML public static TomlTable Parse(TextReader reader) { - using var parser = new TOMLParser(reader) {ForceASCII = ForceASCII}; + using var parser = new TOMLParser(reader) { ForceASCII = ForceASCII }; return parser.Parse(); } } @@ -1960,7 +1960,7 @@ public static bool IsIntegerWithBase(string s, out int numberBase) public const char LITERAL_STRING_SYMBOL = '\''; public const char INT_NUMBER_SEPARATOR = '_'; - public static readonly char[] NewLineCharacters = {NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER}; + public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER }; public static bool IsQuoted(char c) => c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL; @@ -2013,7 +2013,7 @@ public static string Join(this string self, IEnumerable subItems) } public delegate bool TryDateParseDelegate(string s, string format, IFormatProvider ci, DateTimeStyles dts, out T dt); - + public static bool TryParseDateTime(string s, string[] formats, DateTimeStyles styles, @@ -2057,17 +2057,17 @@ public static string Escape(this string txt, bool escapeNewlines = true) static string CodePoint(string txt, ref int i, char c) => char.IsSurrogatePair(txt, i) ? $"\\U{char.ConvertToUtf32(txt, i++):X8}" - : $"\\u{(ushort) c:X4}"; + : $"\\u{(ushort)c:X4}"; stringBuilder.Append(c switch { - '\b' => @"\b", - '\t' => @"\t", + '\b' => @"\b", + '\t' => @"\t", '\n' when escapeNewlines => @"\n", - '\f' => @"\f", + '\f' => @"\f", '\r' when escapeNewlines => @"\r", - '\\' => @"\\", - '\"' => @"\""", + '\\' => @"\\", + '\"' => @"\""", var _ when TomlSyntax.MustBeEscaped(c, !escapeNewlines) || TOML.ForceASCII && c > sbyte.MaxValue => CodePoint(txt, ref i, c), var _ => c @@ -2092,7 +2092,7 @@ public static bool TryUnescape(this string txt, out string unescaped, out Except return false; } } - + public static string Unescape(this string txt) { if (string.IsNullOrEmpty(txt)) return txt; @@ -2115,16 +2115,16 @@ static string CodePoint(int next, string txt, ref int num, int size) stringBuilder.Append(c switch { - 'b' => "\b", - 't' => "\t", - 'n' => "\n", - 'f' => "\f", - 'r' => "\r", - '\'' => "\'", - '\"' => "\"", - '\\' => "\\", - 'u' => CodePoint(next, txt, ref num, 4), - 'U' => CodePoint(next, txt, ref num, 8), + 'b' => "\b", + 't' => "\t", + 'n' => "\n", + 'f' => "\f", + 'r' => "\r", + '\'' => "\'", + '\"' => "\"", + '\\' => "\\", + 'u' => CodePoint(next, txt, ref num, 4), + 'U' => CodePoint(next, txt, ref num, 8), var _ => throw new Exception("Undefined escape sequence!") }); i = num + 2; diff --git a/UnityMcpBridge/Editor/Helpers/ExecPath.cs b/UnityMcpBridge/Editor/Helpers/ExecPath.cs index 5130a21c..20c1200b 100644 --- a/UnityMcpBridge/Editor/Helpers/ExecPath.cs +++ b/UnityMcpBridge/Editor/Helpers/ExecPath.cs @@ -205,7 +205,7 @@ internal static bool TryRun( var so = new StringBuilder(); var se = new StringBuilder(); process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); }; - process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); }; + process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); }; if (!process.Start()) return false; @@ -276,5 +276,3 @@ private static string Where(string exe) #endif } } - - diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs index b143f487..d7bf979c 100644 --- a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs @@ -124,7 +124,7 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ // --- Add Early Logging --- // Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})"); // --- End Early Logging --- - + if (c == null) return null; Type componentType = c.GetType(); @@ -150,8 +150,8 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ { "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 }, { "childCount", tr.childCount }, // Include standard Object/Component properties - { "name", tr.name }, - { "tag", tr.tag }, + { "name", tr.name }, + { "tag", tr.tag }, { "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 } }; } @@ -244,8 +244,9 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ // Basic filtering (readable, not indexer, not transform which is handled elsewhere) if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; // Add if not already added (handles overrides - keep the most derived version) - if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) { - propertiesToCache.Add(propInfo); + if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) + { + propertiesToCache.Add(propInfo); } } @@ -258,8 +259,8 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ { if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields - // Add if not already added (handles hiding - keep the most derived version) - if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; + // Add if not already added (handles hiding - keep the most derived version) + if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; bool shouldInclude = false; if (includeNonPublicSerializedFields) @@ -291,7 +292,7 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ // --- Use cached metadata --- var serializablePropertiesOutput = new Dictionary(); - + // --- Add Logging Before Property Loop --- // Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}..."); // --- End Logging Before Property Loop --- @@ -310,16 +311,16 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ propName == "particleSystem" || // Also skip potentially problematic Matrix properties prone to cycles/errors propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") - { - // Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log - skipProperty = true; - } + { + // Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log + skipProperty = true; + } // --- End Skip Generic Properties --- // --- Skip specific potentially problematic Camera properties --- - if (componentType == typeof(Camera) && - (propName == "pixelRect" || - propName == "rect" || + if (componentType == typeof(Camera) && + (propName == "pixelRect" || + propName == "rect" || propName == "cullingMatrix" || propName == "useOcclusionCulling" || propName == "worldToCameraMatrix" || @@ -334,8 +335,8 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ // --- End Skip Camera Properties --- // --- Skip specific potentially problematic Transform properties --- - if (componentType == typeof(Transform) && - (propName == "lossyScale" || + if (componentType == typeof(Transform) && + (propName == "lossyScale" || propName == "rotation" || propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")) @@ -345,11 +346,11 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ } // --- End Skip Transform Properties --- - // Skip if flagged - if (skipProperty) - { + // Skip if flagged + if (skipProperty) + { continue; - } + } try { @@ -362,7 +363,7 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ } catch (Exception) { - // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}"); + // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}"); } } @@ -373,7 +374,7 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ // Use cached fields foreach (var fieldInfo in cachedData.SerializableFields) { - try + try { // --- Add detailed logging for fields --- // Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}"); @@ -385,7 +386,7 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ } catch (Exception) { - // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}"); + // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}"); } } // --- End Use cached metadata --- @@ -458,19 +459,19 @@ private static object ConvertJTokenToPlainObject(JToken token) case JTokenType.Boolean: return token.ToObject(); case JTokenType.Date: - return token.ToObject(); - case JTokenType.Guid: - return token.ToObject(); - case JTokenType.Uri: - return token.ToObject(); - case JTokenType.TimeSpan: - return token.ToObject(); + return token.ToObject(); + case JTokenType.Guid: + return token.ToObject(); + case JTokenType.Uri: + return token.ToObject(); + case JTokenType.TimeSpan: + return token.ToObject(); case JTokenType.Bytes: - return token.ToObject(); + return token.ToObject(); case JTokenType.Null: return null; - case JTokenType.Undefined: - return null; // Treat undefined as null + case JTokenType.Undefined: + return null; // Treat undefined as null default: // Fallback for simple value types not explicitly listed @@ -524,4 +525,4 @@ private static JToken CreateTokenFromValue(object value, Type type) } } } -} \ No newline at end of file +} diff --git a/UnityMcpBridge/Editor/Helpers/McpConfigFileHelper.cs b/UnityMcpBridge/Editor/Helpers/McpConfigFileHelper.cs index 9b2e5b86..389d47d2 100644 --- a/UnityMcpBridge/Editor/Helpers/McpConfigFileHelper.cs +++ b/UnityMcpBridge/Editor/Helpers/McpConfigFileHelper.cs @@ -184,4 +184,3 @@ public static string ResolveServerSource() } } } - diff --git a/UnityMcpBridge/Editor/Helpers/McpLog.cs b/UnityMcpBridge/Editor/Helpers/McpLog.cs index 7e467187..85abdb79 100644 --- a/UnityMcpBridge/Editor/Helpers/McpLog.cs +++ b/UnityMcpBridge/Editor/Helpers/McpLog.cs @@ -29,5 +29,3 @@ public static void Error(string message) } } } - - diff --git a/UnityMcpBridge/Editor/Helpers/PackageDetector.cs b/UnityMcpBridge/Editor/Helpers/PackageDetector.cs index d39685c2..bb8861fe 100644 --- a/UnityMcpBridge/Editor/Helpers/PackageDetector.cs +++ b/UnityMcpBridge/Editor/Helpers/PackageDetector.cs @@ -105,5 +105,3 @@ private static bool LegacyRootsExist() } } } - - diff --git a/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs b/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs index be9f0a41..795256a7 100644 --- a/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs @@ -10,7 +10,7 @@ namespace MCPForUnity.Editor.Helpers public static class PackageInstaller { private const string InstallationFlagKey = "MCPForUnity.ServerInstalled"; - + static PackageInstaller() { // Check if this is the first time the package is loaded @@ -20,17 +20,17 @@ static PackageInstaller() EditorApplication.delayCall += InstallServerOnFirstLoad; } } - + private static void InstallServerOnFirstLoad() { try { Debug.Log("MCP-FOR-UNITY: Installing Python server..."); ServerInstaller.EnsureServerInstalled(); - + // Mark as installed EditorPrefs.SetBool(InstallationFlagKey, true); - + Debug.Log("MCP-FOR-UNITY: Python server installation completed successfully."); } catch (System.Exception ex) diff --git a/UnityMcpBridge/Editor/Helpers/PortManager.cs b/UnityMcpBridge/Editor/Helpers/PortManager.cs index f041ac23..09d85798 100644 --- a/UnityMcpBridge/Editor/Helpers/PortManager.cs +++ b/UnityMcpBridge/Editor/Helpers/PortManager.cs @@ -43,8 +43,8 @@ public static int GetPortWithFallback() { // Try to load stored port first, but only if it's from the current project var storedConfig = GetStoredPortConfig(); - if (storedConfig != null && - storedConfig.unity_port > 0 && + if (storedConfig != null && + storedConfig.unity_port > 0 && string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase) && IsPortAvailable(storedConfig.unity_port)) { @@ -228,7 +228,7 @@ private static int LoadStoredPort() try { string registryFile = GetRegistryFilePath(); - + if (!File.Exists(registryFile)) { // Backwards compatibility: try the legacy file name @@ -261,7 +261,7 @@ public static PortConfig GetStoredPortConfig() try { string registryFile = GetRegistryFilePath(); - + if (!File.Exists(registryFile)) { // Backwards compatibility: try the legacy file @@ -316,4 +316,4 @@ private static string ComputeProjectHash(string input) } } } -} \ No newline at end of file +} diff --git a/UnityMcpBridge/Editor/Helpers/Response.cs b/UnityMcpBridge/Editor/Helpers/Response.cs index 1a3bd520..cfcd2efb 100644 --- a/UnityMcpBridge/Editor/Helpers/Response.cs +++ b/UnityMcpBridge/Editor/Helpers/Response.cs @@ -60,4 +60,3 @@ public static object Error(string errorCodeOrMessage, object data = null) } } } - diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index 56cf0952..26c0fbb2 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -419,7 +419,7 @@ private static void CopyDirectoryRecursive(string sourceDir, string destinationD try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { } string destSubDir = Path.Combine(destinationDir, dirName); CopyDirectoryRecursive(dirPath, destSubDir); - NextDir: ; + NextDir:; } } @@ -467,7 +467,7 @@ public static bool RepairPythonEnvironment() string uvPath = FindUvPath(); if (uvPath == null) { - Debug.LogError("UV not found. Please install uv (https://docs.astral.sh/uv/)." ); + Debug.LogError("UV not found. Please install uv (https://docs.astral.sh/uv/)."); return false; } @@ -486,7 +486,7 @@ public static bool RepairPythonEnvironment() var sbOut = new StringBuilder(); var sbErr = new StringBuilder(); proc.OutputDataReceived += (_, e) => { if (e.Data != null) sbOut.AppendLine(e.Data); }; - proc.ErrorDataReceived += (_, e) => { if (e.Data != null) sbErr.AppendLine(e.Data); }; + proc.ErrorDataReceived += (_, e) => { if (e.Data != null) sbErr.AppendLine(e.Data); }; if (!proc.Start()) { diff --git a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs index 5684b19a..1342dc12 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs @@ -147,5 +147,3 @@ private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageIn } } } - - diff --git a/UnityMcpBridge/Editor/Helpers/TelemetryHelper.cs b/UnityMcpBridge/Editor/Helpers/TelemetryHelper.cs index 4e068e99..6440a675 100644 --- a/UnityMcpBridge/Editor/Helpers/TelemetryHelper.cs +++ b/UnityMcpBridge/Editor/Helpers/TelemetryHelper.cs @@ -14,7 +14,7 @@ public static class TelemetryHelper private const string TELEMETRY_DISABLED_KEY = "MCPForUnity.TelemetryDisabled"; private const string CUSTOMER_UUID_KEY = "MCPForUnity.CustomerUUID"; private static Action> s_sender; - + /// /// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs) /// @@ -24,14 +24,14 @@ public static bool IsEnabled { // Check environment variables first var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY"); - if (!string.IsNullOrEmpty(envDisable) && + if (!string.IsNullOrEmpty(envDisable) && (envDisable.ToLower() == "true" || envDisable == "1")) { return false; } - + var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY"); - if (!string.IsNullOrEmpty(unityMcpDisable) && + if (!string.IsNullOrEmpty(unityMcpDisable) && (unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1")) { return false; @@ -49,7 +49,7 @@ public static bool IsEnabled return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false); } } - + /// /// Get or generate customer UUID for anonymous tracking /// @@ -63,7 +63,7 @@ public static string GetCustomerUUID() } return uuid; } - + /// /// Disable telemetry (stored in EditorPrefs) /// @@ -71,7 +71,7 @@ public static void DisableTelemetry() { UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true); } - + /// /// Enable telemetry (stored in EditorPrefs) /// @@ -79,7 +79,7 @@ public static void EnableTelemetry() { UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false); } - + /// /// Send telemetry data to Python server for processing /// This is a lightweight bridge - the actual telemetry logic is in Python @@ -88,7 +88,7 @@ public static void RecordEvent(string eventType, Dictionary data { if (!IsEnabled) return; - + try { var telemetryData = new Dictionary @@ -100,12 +100,12 @@ public static void RecordEvent(string eventType, Dictionary data ["platform"] = Application.platform.ToString(), ["source"] = "unity_bridge" }; - + if (data != null) { telemetryData["data"] = data; } - + // Send to Python server via existing bridge communication // The Python server will handle actual telemetry transmission SendTelemetryToPythonServer(telemetryData); @@ -119,7 +119,7 @@ public static void RecordEvent(string eventType, Dictionary data } } } - + /// /// Allows the bridge to register a concrete sender for telemetry payloads. /// @@ -144,7 +144,7 @@ public static void RecordBridgeStartup() ["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode() }); } - + /// /// Record bridge connection event /// @@ -154,15 +154,15 @@ public static void RecordBridgeConnection(bool success, string error = null) { ["success"] = success }; - + if (!string.IsNullOrEmpty(error)) { data["error"] = error.Substring(0, Math.Min(200, error.Length)); } - + RecordEvent("bridge_connection", data); } - + /// /// Record tool execution from Unity side /// @@ -174,15 +174,15 @@ public static void RecordToolExecution(string toolName, bool success, float dura ["success"] = success, ["duration_ms"] = Math.Round(durationMs, 2) }; - + if (!string.IsNullOrEmpty(error)) { data["error"] = error.Substring(0, Math.Min(200, error.Length)); } - + RecordEvent("tool_execution_unity", data); } - + private static void SendTelemetryToPythonServer(Dictionary telemetryData) { var sender = Volatile.Read(ref s_sender); @@ -208,17 +208,17 @@ private static void SendTelemetryToPythonServer(Dictionary telem Debug.Log($"MCP-TELEMETRY: {telemetryData["event_type"]}"); } } - + private static bool IsDebugEnabled() { - try - { - return UnityEditor.EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); - } - catch - { - return false; + try + { + return UnityEditor.EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); + } + catch + { + return false; } } } -} \ No newline at end of file +} diff --git a/UnityMcpBridge/Editor/Helpers/Vector3Helper.cs b/UnityMcpBridge/Editor/Helpers/Vector3Helper.cs index 1075a199..41566188 100644 --- a/UnityMcpBridge/Editor/Helpers/Vector3Helper.cs +++ b/UnityMcpBridge/Editor/Helpers/Vector3Helper.cs @@ -22,4 +22,3 @@ public static Vector3 ParseVector3(JArray array) } } } - diff --git a/UnityMcpBridge/Editor/MCPForUnityBridge.cs b/UnityMcpBridge/Editor/MCPForUnityBridge.cs index 326f921e..39312ce4 100644 --- a/UnityMcpBridge/Editor/MCPForUnityBridge.cs +++ b/UnityMcpBridge/Editor/MCPForUnityBridge.cs @@ -54,7 +54,7 @@ private static Dictionary< private static bool isAutoConnectMode = false; private const ulong MaxFrameBytes = 64UL * 1024 * 1024; // 64 MiB hard cap for framed payloads private const int FrameIOTimeoutMs = 30000; // Per-read timeout to avoid stalled clients - + // IO diagnostics private static long _ioSeq = 0; private static void IoInfo(string s) { McpLog.Info(s, always: false); } @@ -90,14 +90,14 @@ public static void StartAutoConnect() currentUnityPort = PortManager.GetPortWithFallback(); Start(); isAutoConnectMode = true; - + // Record telemetry for bridge startup TelemetryHelper.RecordBridgeStartup(); } catch (Exception ex) { Debug.LogError($"Auto-connect failed: {ex.Message}"); - + // Record telemetry for connection failure TelemetryHelper.RecordBridgeConnection(false, ex.Message); throw; @@ -151,7 +151,8 @@ static MCPForUnityBridge() IoInfo($"[IO] ✗ write FAIL tag={item.Tag} reqId={(item.ReqId?.ToString() ?? "?")} {ex.GetType().Name}: {ex.Message}"); } } - }) { IsBackground = true, Name = "MCP-Writer" }; + }) + { IsBackground = true, Name = "MCP-Writer" }; writerThread.Start(); } catch { } @@ -516,160 +517,160 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken lock (clientsLock) { activeClients.Add(client); } try { - // Framed I/O only; legacy mode removed - try - { - if (IsDebugEnabled()) + // Framed I/O only; legacy mode removed + try { - var ep = client.Client?.RemoteEndPoint?.ToString() ?? "unknown"; - Debug.Log($"UNITY-MCP: Client connected {ep}"); + if (IsDebugEnabled()) + { + var ep = client.Client?.RemoteEndPoint?.ToString() ?? "unknown"; + Debug.Log($"UNITY-MCP: Client connected {ep}"); + } } - } - catch { } - // Strict framing: always require FRAMING=1 and frame all I/O - try - { - client.NoDelay = true; - } - catch { } - try - { - string handshake = "WELCOME UNITY-MCP 1 FRAMING=1\n"; - byte[] handshakeBytes = System.Text.Encoding.ASCII.GetBytes(handshake); - using var cts = new CancellationTokenSource(FrameIOTimeoutMs); + catch { } + // Strict framing: always require FRAMING=1 and frame all I/O + try + { + client.NoDelay = true; + } + catch { } + try + { + string handshake = "WELCOME UNITY-MCP 1 FRAMING=1\n"; + byte[] handshakeBytes = System.Text.Encoding.ASCII.GetBytes(handshake); + using var cts = new CancellationTokenSource(FrameIOTimeoutMs); #if NETSTANDARD2_1 || NET6_0_OR_GREATER - await stream.WriteAsync(handshakeBytes.AsMemory(0, handshakeBytes.Length), cts.Token).ConfigureAwait(false); + await stream.WriteAsync(handshakeBytes.AsMemory(0, handshakeBytes.Length), cts.Token).ConfigureAwait(false); #else await stream.WriteAsync(handshakeBytes, 0, handshakeBytes.Length, cts.Token).ConfigureAwait(false); #endif - if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info("Sent handshake FRAMING=1 (strict)", always: false); - } - catch (Exception ex) - { - if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Warn($"Handshake failed: {ex.Message}"); - return; // abort this client - } - - while (isRunning && !token.IsCancellationRequested) - { - try + if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info("Sent handshake FRAMING=1 (strict)", always: false); + } + catch (Exception ex) { - // Strict framed mode only: enforced framed I/O for this connection - string commandText = await ReadFrameAsUtf8Async(stream, FrameIOTimeoutMs, token).ConfigureAwait(false); + if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Warn($"Handshake failed: {ex.Message}"); + return; // abort this client + } + while (isRunning && !token.IsCancellationRequested) + { try { - if (IsDebugEnabled()) + // Strict framed mode only: enforced framed I/O for this connection + string commandText = await ReadFrameAsUtf8Async(stream, FrameIOTimeoutMs, token).ConfigureAwait(false); + + try { - var preview = commandText.Length > 120 ? commandText.Substring(0, 120) + "…" : commandText; - MCPForUnity.Editor.Helpers.McpLog.Info($"recv framed: {preview}", always: false); + if (IsDebugEnabled()) + { + var preview = commandText.Length > 120 ? commandText.Substring(0, 120) + "…" : commandText; + MCPForUnity.Editor.Helpers.McpLog.Info($"recv framed: {preview}", always: false); + } } - } - catch { } - string commandId = Guid.NewGuid().ToString(); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + catch { } + string commandId = Guid.NewGuid().ToString(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // Special handling for ping command to avoid JSON parsing - if (commandText.Trim() == "ping") - { - // Direct response to ping without going through JSON parsing - byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes( - /*lang=json,strict*/ - "{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}" - ); - await WriteFrameAsync(stream, pingResponseBytes); - continue; - } + // Special handling for ping command to avoid JSON parsing + if (commandText.Trim() == "ping") + { + // Direct response to ping without going through JSON parsing + byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes( + /*lang=json,strict*/ + "{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}" + ); + await WriteFrameAsync(stream, pingResponseBytes); + continue; + } - lock (lockObj) - { - commandQueue[commandId] = (commandText, tcs); - } + lock (lockObj) + { + commandQueue[commandId] = (commandText, tcs); + } - // Wait for the handler to produce a response, but do not block indefinitely - string response; - try - { - using var respCts = new CancellationTokenSource(FrameIOTimeoutMs); - var completed = await Task.WhenAny(tcs.Task, Task.Delay(FrameIOTimeoutMs, respCts.Token)).ConfigureAwait(false); - if (completed == tcs.Task) + // Wait for the handler to produce a response, but do not block indefinitely + string response; + try { - // Got a result from the handler - respCts.Cancel(); - response = tcs.Task.Result; + using var respCts = new CancellationTokenSource(FrameIOTimeoutMs); + var completed = await Task.WhenAny(tcs.Task, Task.Delay(FrameIOTimeoutMs, respCts.Token)).ConfigureAwait(false); + if (completed == tcs.Task) + { + // Got a result from the handler + respCts.Cancel(); + response = tcs.Task.Result; + } + else + { + // Timeout: return a structured error so the client can recover + var timeoutResponse = new + { + status = "error", + error = $"Command processing timed out after {FrameIOTimeoutMs} ms", + }; + response = JsonConvert.SerializeObject(timeoutResponse); + } } - else + catch (Exception ex) { - // Timeout: return a structured error so the client can recover - var timeoutResponse = new + var errorResponse = new { status = "error", - error = $"Command processing timed out after {FrameIOTimeoutMs} ms", + error = ex.Message, }; - response = JsonConvert.SerializeObject(timeoutResponse); + response = JsonConvert.SerializeObject(errorResponse); } - } - catch (Exception ex) - { - var errorResponse = new - { - status = "error", - error = ex.Message, - }; - response = JsonConvert.SerializeObject(errorResponse); - } - if (IsDebugEnabled()) - { - try { MCPForUnity.Editor.Helpers.McpLog.Info("[MCP] sending framed response", always: false); } catch { } - } - // Crash-proof and self-reporting writer logs (direct write to this client's stream) - long seq = System.Threading.Interlocked.Increment(ref _ioSeq); - byte[] responseBytes; - try - { - responseBytes = System.Text.Encoding.UTF8.GetBytes(response); - IoInfo($"[IO] ➜ write start seq={seq} tag=response len={responseBytes.Length} reqId=?"); - } - catch (Exception ex) - { - IoInfo($"[IO] ✗ serialize FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}"); - throw; - } + if (IsDebugEnabled()) + { + try { MCPForUnity.Editor.Helpers.McpLog.Info("[MCP] sending framed response", always: false); } catch { } + } + // Crash-proof and self-reporting writer logs (direct write to this client's stream) + long seq = System.Threading.Interlocked.Increment(ref _ioSeq); + byte[] responseBytes; + try + { + responseBytes = System.Text.Encoding.UTF8.GetBytes(response); + IoInfo($"[IO] ➜ write start seq={seq} tag=response len={responseBytes.Length} reqId=?"); + } + catch (Exception ex) + { + IoInfo($"[IO] ✗ serialize FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}"); + throw; + } - var swDirect = System.Diagnostics.Stopwatch.StartNew(); - try - { - await WriteFrameAsync(stream, responseBytes); - swDirect.Stop(); - IoInfo($"[IO] ✓ write end tag=response len={responseBytes.Length} reqId=? durMs={swDirect.Elapsed.TotalMilliseconds:F1}"); + var swDirect = System.Diagnostics.Stopwatch.StartNew(); + try + { + await WriteFrameAsync(stream, responseBytes); + swDirect.Stop(); + IoInfo($"[IO] ✓ write end tag=response len={responseBytes.Length} reqId=? durMs={swDirect.Elapsed.TotalMilliseconds:F1}"); + } + catch (Exception ex) + { + IoInfo($"[IO] ✗ write FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}"); + throw; + } } catch (Exception ex) { - IoInfo($"[IO] ✗ write FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}"); - throw; - } - } - catch (Exception ex) - { - // Treat common disconnects/timeouts as benign; only surface hard errors - string msg = ex.Message ?? string.Empty; - bool isBenign = - msg.IndexOf("Connection closed before reading expected bytes", StringComparison.OrdinalIgnoreCase) >= 0 - || msg.IndexOf("Read timed out", StringComparison.OrdinalIgnoreCase) >= 0 - || ex is System.IO.IOException; - if (isBenign) - { - if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info($"Client handler: {msg}", always: false); - } - else - { - MCPForUnity.Editor.Helpers.McpLog.Error($"Client handler error: {msg}"); + // Treat common disconnects/timeouts as benign; only surface hard errors + string msg = ex.Message ?? string.Empty; + bool isBenign = + msg.IndexOf("Connection closed before reading expected bytes", StringComparison.OrdinalIgnoreCase) >= 0 + || msg.IndexOf("Read timed out", StringComparison.OrdinalIgnoreCase) >= 0 + || ex is System.IO.IOException; + if (isBenign) + { + if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info($"Client handler: {msg}", always: false); + } + else + { + MCPForUnity.Editor.Helpers.McpLog.Error($"Client handler error: {msg}"); + } + break; } - break; } } - } finally { lock (clientsLock) { activeClients.Remove(client); } @@ -806,116 +807,116 @@ private static void ProcessCommands() if (Interlocked.Exchange(ref processingCommands, 1) == 1) return; // reentrancy guard try { - // Heartbeat without holding the queue lock - double now = EditorApplication.timeSinceStartup; - if (now >= nextHeartbeatAt) - { - WriteHeartbeat(false); - nextHeartbeatAt = now + 0.5f; - } - - // Snapshot under lock, then process outside to reduce contention - List<(string id, string text, TaskCompletionSource tcs)> work; - lock (lockObj) - { - work = commandQueue - .Select(kvp => (kvp.Key, kvp.Value.commandJson, kvp.Value.tcs)) - .ToList(); - } + // Heartbeat without holding the queue lock + double now = EditorApplication.timeSinceStartup; + if (now >= nextHeartbeatAt) + { + WriteHeartbeat(false); + nextHeartbeatAt = now + 0.5f; + } - foreach (var item in work) - { - string id = item.id; - string commandText = item.text; - TaskCompletionSource tcs = item.tcs; + // Snapshot under lock, then process outside to reduce contention + List<(string id, string text, TaskCompletionSource tcs)> work; + lock (lockObj) + { + work = commandQueue + .Select(kvp => (kvp.Key, kvp.Value.commandJson, kvp.Value.tcs)) + .ToList(); + } - try + foreach (var item in work) { - // Special case handling - if (string.IsNullOrEmpty(commandText)) + string id = item.id; + string commandText = item.text; + TaskCompletionSource tcs = item.tcs; + + try { - var emptyResponse = new + // Special case handling + if (string.IsNullOrEmpty(commandText)) { - status = "error", - error = "Empty command received", - }; - tcs.SetResult(JsonConvert.SerializeObject(emptyResponse)); - // Remove quickly under lock - lock (lockObj) { commandQueue.Remove(id); } - continue; - } + var emptyResponse = new + { + status = "error", + error = "Empty command received", + }; + tcs.SetResult(JsonConvert.SerializeObject(emptyResponse)); + // Remove quickly under lock + lock (lockObj) { commandQueue.Remove(id); } + continue; + } - // Trim the command text to remove any whitespace - commandText = commandText.Trim(); + // Trim the command text to remove any whitespace + commandText = commandText.Trim(); - // Non-JSON direct commands handling (like ping) - if (commandText == "ping") - { - var pingResponse = new + // Non-JSON direct commands handling (like ping) + if (commandText == "ping") { - status = "success", - result = new { message = "pong" }, - }; - tcs.SetResult(JsonConvert.SerializeObject(pingResponse)); - lock (lockObj) { commandQueue.Remove(id); } - continue; - } + var pingResponse = new + { + status = "success", + result = new { message = "pong" }, + }; + tcs.SetResult(JsonConvert.SerializeObject(pingResponse)); + lock (lockObj) { commandQueue.Remove(id); } + continue; + } - // Check if the command is valid JSON before attempting to deserialize - if (!IsValidJson(commandText)) - { - var invalidJsonResponse = new + // Check if the command is valid JSON before attempting to deserialize + if (!IsValidJson(commandText)) { - status = "error", - error = "Invalid JSON format", - receivedText = commandText.Length > 50 - ? commandText[..50] + "..." - : commandText, - }; - tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse)); - lock (lockObj) { commandQueue.Remove(id); } - continue; - } + var invalidJsonResponse = new + { + status = "error", + error = "Invalid JSON format", + receivedText = commandText.Length > 50 + ? commandText[..50] + "..." + : commandText, + }; + tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse)); + lock (lockObj) { commandQueue.Remove(id); } + continue; + } - // Normal JSON command processing - Command command = JsonConvert.DeserializeObject(commandText); + // Normal JSON command processing + Command command = JsonConvert.DeserializeObject(commandText); - if (command == null) + if (command == null) + { + var nullCommandResponse = new + { + status = "error", + error = "Command deserialized to null", + details = "The command was valid JSON but could not be deserialized to a Command object", + }; + tcs.SetResult(JsonConvert.SerializeObject(nullCommandResponse)); + } + else + { + string responseJson = ExecuteCommand(command); + tcs.SetResult(responseJson); + } + } + catch (Exception ex) { - var nullCommandResponse = new + Debug.LogError($"Error processing command: {ex.Message}\n{ex.StackTrace}"); + + var response = new { status = "error", - error = "Command deserialized to null", - details = "The command was valid JSON but could not be deserialized to a Command object", + error = ex.Message, + commandType = "Unknown (error during processing)", + receivedText = commandText?.Length > 50 + ? commandText[..50] + "..." + : commandText, }; - tcs.SetResult(JsonConvert.SerializeObject(nullCommandResponse)); - } - else - { - string responseJson = ExecuteCommand(command); + string responseJson = JsonConvert.SerializeObject(response); tcs.SetResult(responseJson); } - } - catch (Exception ex) - { - Debug.LogError($"Error processing command: {ex.Message}\n{ex.StackTrace}"); - var response = new - { - status = "error", - error = ex.Message, - commandType = "Unknown (error during processing)", - receivedText = commandText?.Length > 50 - ? commandText[..50] + "..." - : commandText, - }; - string responseJson = JsonConvert.SerializeObject(response); - tcs.SetResult(responseJson); + // Remove quickly under lock + lock (lockObj) { commandQueue.Remove(id); } } - - // Remove quickly under lock - lock (lockObj) { commandQueue.Remove(id); } - } } finally { diff --git a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs index afc14448..2503391d 100644 --- a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs +++ b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs @@ -48,4 +48,3 @@ public static void Add(string commandName, Func handler) } } } - diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 4c11f343..71a379b0 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -27,7 +27,7 @@ public static class ManageGameObject Converters = new List { new Vector3Converter(), - new Vector2Converter(), + new Vector2Converter(), new QuaternionConverter(), new ColorConverter(), new RectConverter(), @@ -35,7 +35,7 @@ public static class ManageGameObject new UnityEngineObjectConverter() } }); - + // --- Main Handler --- public static object HandleCommand(JObject @params) @@ -879,7 +879,7 @@ string searchMethod // return Response.Success( // $"GameObject '{targetGo.name}' modified successfully.", // GetGameObjectData(targetGo)); - + } private static object DeleteGameObject(JToken targetToken, string searchMethod) @@ -962,23 +962,23 @@ private static object GetComponentsFromTarget(string target, string searchMethod // --- Get components, immediately copy to list, and null original array --- Component[] originalComponents = targetGo.GetComponents(); List componentsToIterate = new List(originalComponents ?? Array.Empty()); // Copy immediately, handle null case - int componentCount = componentsToIterate.Count; + int componentCount = componentsToIterate.Count; originalComponents = null; // Null the original reference - // Debug.Log($"[GetComponentsFromTarget] Found {componentCount} components on {targetGo.name}. Copied to list, nulled original. Starting REVERSE for loop..."); - // --- End Copy and Null --- - + // Debug.Log($"[GetComponentsFromTarget] Found {componentCount} components on {targetGo.name}. Copied to list, nulled original. Starting REVERSE for loop..."); + // --- End Copy and Null --- + var componentData = new List(); - + for (int i = componentCount - 1; i >= 0; i--) // Iterate backwards over the COPY { Component c = componentsToIterate[i]; // Use the copy - if (c == null) + if (c == null) { // Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] Encountered a null component at index {i} on {targetGo.name}. Skipping."); continue; // Safety check } // Debug.Log($"[GetComponentsFromTarget REVERSE for] Processing component: {c.GetType()?.FullName ?? "null"} (ID: {c.GetInstanceID()}) at index {i} on {targetGo.name}"); - try + try { var data = Helpers.GameObjectSerializer.GetComponentData(c, includeNonPublicSerialized); if (data != null) // Ensure GetComponentData didn't return null @@ -1002,7 +1002,7 @@ private static object GetComponentsFromTarget(string target, string searchMethod } } // Debug.Log($"[GetComponentsFromTarget] Finished REVERSE for loop."); - + // Cleanup the list we created componentsToIterate.Clear(); componentsToIterate = null; @@ -1181,7 +1181,7 @@ string searchMethod return removeResult; // Return error EditorUtility.SetDirty(targetGo); - // Use the new serializer helper + // Use the new serializer helper return Response.Success( $"Component '{typeName}' removed from '{targetGo.name}'.", Helpers.GameObjectSerializer.GetGameObjectData(targetGo) @@ -1230,7 +1230,7 @@ string searchMethod return setResult; // Return error EditorUtility.SetDirty(targetGo); - // Use the new serializer helper + // Use the new serializer helper return Response.Success( $"Properties set for component '{compName}' on '{targetGo.name}'.", Helpers.GameObjectSerializer.GetGameObjectData(targetGo) @@ -1693,8 +1693,8 @@ private static bool SetProperty(object target, string memberName, JToken value) BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - // Use shared serializer to avoid per-call allocation - var inputSerializer = InputSerializer; + // Use shared serializer to avoid per-call allocation + var inputSerializer = InputSerializer; try { @@ -1716,8 +1716,9 @@ private static bool SetProperty(object target, string memberName, JToken value) propInfo.SetValue(target, convertedValue); return true; } - else { - Debug.LogWarning($"[SetProperty] Conversion failed for property '{memberName}' (Type: {propInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); + else + { + Debug.LogWarning($"[SetProperty] Conversion failed for property '{memberName}' (Type: {propInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); } } else @@ -1725,16 +1726,17 @@ private static bool SetProperty(object target, string memberName, JToken value) FieldInfo fieldInfo = type.GetField(memberName, flags); if (fieldInfo != null) // Check if !IsLiteral? { - // Use the inputSerializer for conversion + // Use the inputSerializer for conversion object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType, inputSerializer); - if (convertedValue != null || value.Type == JTokenType.Null) // Allow setting null + if (convertedValue != null || value.Type == JTokenType.Null) // Allow setting null { fieldInfo.SetValue(target, convertedValue); return true; } - else { - Debug.LogWarning($"[SetProperty] Conversion failed for field '{memberName}' (Type: {fieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); - } + else + { + Debug.LogWarning($"[SetProperty] Conversion failed for field '{memberName}' (Type: {fieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); + } } else { @@ -1881,12 +1883,17 @@ private static bool SetNestedProperty(object target, string path, JToken value, if (value is JArray jArray) { // Try converting to known types that SetColor/SetVector accept - if (jArray.Count == 4) { + if (jArray.Count == 4) + { try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } try { Vector4 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { } - } else if (jArray.Count == 3) { + } + else if (jArray.Count == 3) + { try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } // ToObject handles conversion to Color - } else if (jArray.Count == 2) { + } + else if (jArray.Count == 2) + { try { Vector2 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { } } } @@ -1901,13 +1908,16 @@ private static bool SetNestedProperty(object target, string path, JToken value, else if (value.Type == JTokenType.String) { // Try converting to Texture using the serializer/converter - try { + try + { Texture texture = value.ToObject(inputSerializer); - if (texture != null) { + if (texture != null) + { material.SetTexture(finalPart, texture); return true; } - } catch { } + } + catch { } } Debug.LogWarning( @@ -1927,7 +1937,8 @@ private static bool SetNestedProperty(object target, string path, JToken value, finalPropInfo.SetValue(currentObject, convertedValue); return true; } - else { + else + { Debug.LogWarning($"[SetNestedProperty] Final conversion failed for property '{finalPart}' (Type: {finalPropInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); } } @@ -1943,7 +1954,8 @@ private static bool SetNestedProperty(object target, string path, JToken value, finalFieldInfo.SetValue(currentObject, convertedValue); return true; } - else { + else + { Debug.LogWarning($"[SetNestedProperty] Final conversion failed for field '{finalPart}' (Type: {finalFieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); } } @@ -2025,25 +2037,25 @@ private static object ConvertJTokenToType(JToken token, Type targetType, JsonSer } catch (JsonSerializationException jsonEx) { - Debug.LogError($"JSON Deserialization Error converting token to {targetType.FullName}: {jsonEx.Message}\nToken: {token.ToString(Formatting.None)}"); - // Optionally re-throw or return null/default - // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; - throw; // Re-throw to indicate failure higher up + Debug.LogError($"JSON Deserialization Error converting token to {targetType.FullName}: {jsonEx.Message}\nToken: {token.ToString(Formatting.None)}"); + // Optionally re-throw or return null/default + // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + throw; // Re-throw to indicate failure higher up } catch (ArgumentException argEx) { Debug.LogError($"Argument Error converting token to {targetType.FullName}: {argEx.Message}\nToken: {token.ToString(Formatting.None)}"); - throw; + throw; } catch (Exception ex) { - Debug.LogError($"Unexpected error converting token to {targetType.FullName}: {ex}\nToken: {token.ToString(Formatting.None)}"); - throw; + Debug.LogError($"Unexpected error converting token to {targetType.FullName}: {ex}\nToken: {token.ToString(Formatting.None)}"); + throw; } // If ToObject succeeded, it would have returned. If it threw, we wouldn't reach here. - // This fallback logic is likely unreachable if ToObject covers all cases or throws on failure. - // Debug.LogWarning($"Conversion failed for token to {targetType.FullName}. Token: {token.ToString(Formatting.None)}"); - // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + // This fallback logic is likely unreachable if ToObject covers all cases or throws on failure. + // Debug.LogWarning($"Conversion failed for token to {targetType.FullName}. Token: {token.ToString(Formatting.None)}"); + // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; } // --- ParseJTokenTo... helpers are likely redundant now with the serializer approach --- @@ -2059,7 +2071,7 @@ private static Vector3 ParseJTokenToVector3(JToken token) } if (token is JArray arr && arr.Count >= 3) { - return new Vector3(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject()); + return new Vector3(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject()); } Debug.LogWarning($"Could not parse JToken '{token}' as Vector3 using fallback. Returning Vector3.zero."); return Vector3.zero; @@ -2068,13 +2080,13 @@ private static Vector3 ParseJTokenToVector3(JToken token) private static Vector2 ParseJTokenToVector2(JToken token) { // ... (implementation - likely replaced by Vector2Converter) ... - if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y")) + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y")) { return new Vector2(obj["x"].ToObject(), obj["y"].ToObject()); } if (token is JArray arr && arr.Count >= 2) { - return new Vector2(arr[0].ToObject(), arr[1].ToObject()); + return new Vector2(arr[0].ToObject(), arr[1].ToObject()); } Debug.LogWarning($"Could not parse JToken '{token}' as Vector2 using fallback. Returning Vector2.zero."); return Vector2.zero; @@ -2088,47 +2100,47 @@ private static Quaternion ParseJTokenToQuaternion(JToken token) } if (token is JArray arr && arr.Count >= 4) { - return new Quaternion(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + return new Quaternion(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); } Debug.LogWarning($"Could not parse JToken '{token}' as Quaternion using fallback. Returning Quaternion.identity."); return Quaternion.identity; } private static Color ParseJTokenToColor(JToken token) { - // ... (implementation - likely replaced by ColorConverter) ... + // ... (implementation - likely replaced by ColorConverter) ... if (token is JObject obj && obj.ContainsKey("r") && obj.ContainsKey("g") && obj.ContainsKey("b") && obj.ContainsKey("a")) { return new Color(obj["r"].ToObject(), obj["g"].ToObject(), obj["b"].ToObject(), obj["a"].ToObject()); } if (token is JArray arr && arr.Count >= 4) { - return new Color(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + return new Color(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); } Debug.LogWarning($"Could not parse JToken '{token}' as Color using fallback. Returning Color.white."); return Color.white; } private static Rect ParseJTokenToRect(JToken token) { - // ... (implementation - likely replaced by RectConverter) ... + // ... (implementation - likely replaced by RectConverter) ... if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("width") && obj.ContainsKey("height")) { return new Rect(obj["x"].ToObject(), obj["y"].ToObject(), obj["width"].ToObject(), obj["height"].ToObject()); } if (token is JArray arr && arr.Count >= 4) { - return new Rect(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + return new Rect(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); } Debug.LogWarning($"Could not parse JToken '{token}' as Rect using fallback. Returning Rect.zero."); return Rect.zero; } private static Bounds ParseJTokenToBounds(JToken token) { - // ... (implementation - likely replaced by BoundsConverter) ... + // ... (implementation - likely replaced by BoundsConverter) ... if (token is JObject obj && obj.ContainsKey("center") && obj.ContainsKey("size")) { // Requires Vector3 conversion, which should ideally use the serializer too - Vector3 center = ParseJTokenToVector3(obj["center"]); // Or use obj["center"].ToObject(inputSerializer) - Vector3 size = ParseJTokenToVector3(obj["size"]); // Or use obj["size"].ToObject(inputSerializer) + Vector3 center = ParseJTokenToVector3(obj["center"]); // Or use obj["center"].ToObject(inputSerializer) + Vector3 size = ParseJTokenToVector3(obj["size"]); // Or use obj["size"].ToObject(inputSerializer) return new Bounds(center, size); } // Array fallback for Bounds is less intuitive, maybe remove? @@ -2141,109 +2153,109 @@ private static Bounds ParseJTokenToBounds(JToken token) } // --- End Redundant Parse Helpers --- - /// - /// Finds a specific UnityEngine.Object based on a find instruction JObject. - /// Primarily used by UnityEngineObjectConverter during deserialization. - /// - // Made public static so UnityEngineObjectConverter can call it. Moved from ConvertJTokenToType. - public static UnityEngine.Object FindObjectByInstruction(JObject instruction, Type targetType) - { - string findTerm = instruction["find"]?.ToString(); - string method = instruction["method"]?.ToString()?.ToLower(); - string componentName = instruction["component"]?.ToString(); // Specific component to get - - if (string.IsNullOrEmpty(findTerm)) - { - Debug.LogWarning("Find instruction missing 'find' term."); - return null; - } - - // Use a flexible default search method if none provided - string searchMethodToUse = string.IsNullOrEmpty(method) ? "by_id_or_name_or_path" : method; - - // If the target is an asset (Material, Texture, ScriptableObject etc.) try AssetDatabase first - if (typeof(Material).IsAssignableFrom(targetType) || - typeof(Texture).IsAssignableFrom(targetType) || - typeof(ScriptableObject).IsAssignableFrom(targetType) || - targetType.FullName.StartsWith("UnityEngine.U2D") || // Sprites etc. - typeof(AudioClip).IsAssignableFrom(targetType) || - typeof(AnimationClip).IsAssignableFrom(targetType) || - typeof(Font).IsAssignableFrom(targetType) || - typeof(Shader).IsAssignableFrom(targetType) || - typeof(ComputeShader).IsAssignableFrom(targetType) || - typeof(GameObject).IsAssignableFrom(targetType) && findTerm.StartsWith("Assets/")) // Prefab check - { + /// + /// Finds a specific UnityEngine.Object based on a find instruction JObject. + /// Primarily used by UnityEngineObjectConverter during deserialization. + /// + // Made public static so UnityEngineObjectConverter can call it. Moved from ConvertJTokenToType. + public static UnityEngine.Object FindObjectByInstruction(JObject instruction, Type targetType) + { + string findTerm = instruction["find"]?.ToString(); + string method = instruction["method"]?.ToString()?.ToLower(); + string componentName = instruction["component"]?.ToString(); // Specific component to get + + if (string.IsNullOrEmpty(findTerm)) + { + Debug.LogWarning("Find instruction missing 'find' term."); + return null; + } + + // Use a flexible default search method if none provided + string searchMethodToUse = string.IsNullOrEmpty(method) ? "by_id_or_name_or_path" : method; + + // If the target is an asset (Material, Texture, ScriptableObject etc.) try AssetDatabase first + if (typeof(Material).IsAssignableFrom(targetType) || + typeof(Texture).IsAssignableFrom(targetType) || + typeof(ScriptableObject).IsAssignableFrom(targetType) || + targetType.FullName.StartsWith("UnityEngine.U2D") || // Sprites etc. + typeof(AudioClip).IsAssignableFrom(targetType) || + typeof(AnimationClip).IsAssignableFrom(targetType) || + typeof(Font).IsAssignableFrom(targetType) || + typeof(Shader).IsAssignableFrom(targetType) || + typeof(ComputeShader).IsAssignableFrom(targetType) || + typeof(GameObject).IsAssignableFrom(targetType) && findTerm.StartsWith("Assets/")) // Prefab check + { // Try loading directly by path/GUID first UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(findTerm, targetType); - if (asset != null) return asset; - asset = AssetDatabase.LoadAssetAtPath(findTerm); // Try generic if type specific failed - if (asset != null && targetType.IsAssignableFrom(asset.GetType())) return asset; - - - // If direct path failed, try finding by name/type using FindAssets - string searchFilter = $"t:{targetType.Name} {System.IO.Path.GetFileNameWithoutExtension(findTerm)}"; // Search by type and name - string[] guids = AssetDatabase.FindAssets(searchFilter); - - if (guids.Length == 1) - { - asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0]), targetType); - if (asset != null) return asset; - } - else if (guids.Length > 1) - { - Debug.LogWarning($"[FindObjectByInstruction] Ambiguous asset find: Found {guids.Length} assets matching filter '{searchFilter}'. Provide a full path or unique name."); - // Optionally return the first one? Or null? Returning null is safer. - return null; - } - // If still not found, fall through to scene search (though unlikely for assets) - } - - - // --- Scene Object Search --- - // Find the GameObject using the internal finder - GameObject foundGo = FindObjectInternal(new JValue(findTerm), searchMethodToUse); - - if (foundGo == null) - { - // Don't warn yet, could still be an asset not found above - // Debug.LogWarning($"Could not find GameObject using instruction: {instruction}"); - return null; - } - - // Now, get the target object/component from the found GameObject - if (targetType == typeof(GameObject)) - { - return foundGo; // We were looking for a GameObject - } - else if (typeof(Component).IsAssignableFrom(targetType)) - { - Type componentToGetType = targetType; - if (!string.IsNullOrEmpty(componentName)) - { - Type specificCompType = FindType(componentName); - if (specificCompType != null && typeof(Component).IsAssignableFrom(specificCompType)) - { - componentToGetType = specificCompType; - } - else - { - Debug.LogWarning($"Could not find component type '{componentName}' specified in find instruction. Falling back to target type '{targetType.Name}'."); - } - } - - Component foundComp = foundGo.GetComponent(componentToGetType); - if (foundComp == null) - { - Debug.LogWarning($"Found GameObject '{foundGo.name}' but could not find component of type '{componentToGetType.Name}'."); - } - return foundComp; - } - else - { - Debug.LogWarning($"Find instruction handling not implemented for target type: {targetType.Name}"); - return null; - } - } + if (asset != null) return asset; + asset = AssetDatabase.LoadAssetAtPath(findTerm); // Try generic if type specific failed + if (asset != null && targetType.IsAssignableFrom(asset.GetType())) return asset; + + + // If direct path failed, try finding by name/type using FindAssets + string searchFilter = $"t:{targetType.Name} {System.IO.Path.GetFileNameWithoutExtension(findTerm)}"; // Search by type and name + string[] guids = AssetDatabase.FindAssets(searchFilter); + + if (guids.Length == 1) + { + asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0]), targetType); + if (asset != null) return asset; + } + else if (guids.Length > 1) + { + Debug.LogWarning($"[FindObjectByInstruction] Ambiguous asset find: Found {guids.Length} assets matching filter '{searchFilter}'. Provide a full path or unique name."); + // Optionally return the first one? Or null? Returning null is safer. + return null; + } + // If still not found, fall through to scene search (though unlikely for assets) + } + + + // --- Scene Object Search --- + // Find the GameObject using the internal finder + GameObject foundGo = FindObjectInternal(new JValue(findTerm), searchMethodToUse); + + if (foundGo == null) + { + // Don't warn yet, could still be an asset not found above + // Debug.LogWarning($"Could not find GameObject using instruction: {instruction}"); + return null; + } + + // Now, get the target object/component from the found GameObject + if (targetType == typeof(GameObject)) + { + return foundGo; // We were looking for a GameObject + } + else if (typeof(Component).IsAssignableFrom(targetType)) + { + Type componentToGetType = targetType; + if (!string.IsNullOrEmpty(componentName)) + { + Type specificCompType = FindType(componentName); + if (specificCompType != null && typeof(Component).IsAssignableFrom(specificCompType)) + { + componentToGetType = specificCompType; + } + else + { + Debug.LogWarning($"Could not find component type '{componentName}' specified in find instruction. Falling back to target type '{targetType.Name}'."); + } + } + + Component foundComp = foundGo.GetComponent(componentToGetType); + if (foundComp == null) + { + Debug.LogWarning($"Found GameObject '{foundGo.name}' but could not find component of type '{componentToGetType.Name}'."); + } + return foundComp; + } + else + { + Debug.LogWarning($"Find instruction handling not implemented for target type: {targetType.Name}"); + return null; + } + } /// @@ -2256,17 +2268,17 @@ private static Type FindType(string typeName) { return resolvedType; } - + // Log the resolver error if type wasn't found if (!string.IsNullOrEmpty(error)) { Debug.LogWarning($"[FindType] {error}"); } - + return null; } } - + /// /// Robust component resolver that avoids Assembly.LoadFrom and supports assembly definitions. /// Prioritizes runtime (Player) assemblies over Editor assemblies. @@ -2445,7 +2457,7 @@ public static List GetAIPropertySuggestions(string userInput, List GetRuleBasedSuggestions(string userInput, List 0 && char.IsUpper(trimmedLine[0]) @@ -568,4 +568,3 @@ May change between versions. */ } } - diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index cdaa6c17..a02193e6 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -31,17 +31,17 @@ public class MCPForUnityEditorWindow : EditorWindow private bool lastBridgeVerifiedOk; private string pythonDirOverride = null; private bool debugLogsEnabled; - + // Script validation settings private int validationLevelIndex = 1; // Default to Standard private readonly string[] validationLevelOptions = new string[] { "Basic - Only syntax checks", - "Standard - Syntax + Unity practices", + "Standard - Syntax + Unity practices", "Comprehensive - All checks + semantic analysis", "Strict - Full semantic validation (requires Roslyn)" }; - + // UI state private int selectedClientIndex = 0; @@ -67,7 +67,7 @@ private void OnEnable() { CheckMcpConfiguration(mcpClient); } - + // Load validation level setting LoadValidationLevelSetting(); @@ -77,7 +77,7 @@ private void OnEnable() AutoFirstRunSetup(); } } - + private void OnFocus() { // Refresh bridge running state on focus in case initialization completed after domain reload @@ -172,7 +172,7 @@ private void OnGUI() // Header DrawHeader(); - + // Compute equal column widths for uniform layout float horizontalSpacing = 2f; float outerPadding = 20f; // approximate padding @@ -226,13 +226,13 @@ private void DrawHeader() EditorGUILayout.Space(15); Rect titleRect = EditorGUILayout.GetControlRect(false, 40); EditorGUI.DrawRect(titleRect, new Color(0.2f, 0.2f, 0.2f, 0.1f)); - + GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleLeft }; - + GUI.Label( new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height), "MCP for Unity Editor", @@ -323,7 +323,7 @@ private static string ReadEmbeddedVersionOrFallback() private void DrawServerStatusSection() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); - + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 @@ -334,7 +334,7 @@ private void DrawServerStatusSection() EditorGUILayout.BeginHorizontal(); Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); DrawStatusDot(statusRect, pythonServerInstallationStatusColor, 16); - + GUIStyle statusStyle = new GUIStyle(EditorStyles.label) { fontSize = 12, @@ -344,14 +344,14 @@ private void DrawServerStatusSection() EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); - + EditorGUILayout.BeginHorizontal(); bool isAutoMode = MCPForUnityBridge.IsAutoConnectMode(); GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); - + int currentUnityPort = MCPForUnityBridge.GetCurrentPort(); GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel) { @@ -441,7 +441,7 @@ private void DrawServerStatusSection() private void DrawBridgeSection() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); - + // Always reflect the live state each repaint to avoid stale UI after recompiles isUnityBridgeRunning = MCPForUnityBridge.IsRunning; @@ -451,12 +451,12 @@ private void DrawBridgeSection() }; EditorGUILayout.LabelField("Unity Bridge", sectionTitleStyle); EditorGUILayout.Space(8); - + EditorGUILayout.BeginHorizontal(); Color bridgeColor = isUnityBridgeRunning ? Color.green : Color.red; Rect bridgeStatusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); DrawStatusDot(bridgeStatusRect, bridgeColor, 16); - + GUIStyle bridgeStatusStyle = new GUIStyle(EditorStyles.label) { fontSize = 12, @@ -477,21 +477,21 @@ private void DrawBridgeSection() private void DrawValidationSection() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); - + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }; EditorGUILayout.LabelField("Script Validation", sectionTitleStyle); EditorGUILayout.Space(8); - + EditorGUI.BeginChangeCheck(); validationLevelIndex = EditorGUILayout.Popup("Validation Level", validationLevelIndex, validationLevelOptions, GUILayout.Height(20)); if (EditorGUI.EndChangeCheck()) { SaveValidationLevelSetting(); } - + EditorGUILayout.Space(8); string description = GetValidationLevelDescription(validationLevelIndex); EditorGUILayout.HelpBox(description, MessageType.Info); @@ -504,15 +504,15 @@ private void DrawValidationSection() private void DrawUnifiedClientConfiguration() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); - + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }; EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle); EditorGUILayout.Space(10); - - // (Auto-connect toggle removed per design) + + // (Auto-connect toggle removed per design) // Client selector string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray(); @@ -522,15 +522,15 @@ private void DrawUnifiedClientConfiguration() { selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1); } - + EditorGUILayout.Space(10); - + if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count) { McpClient selectedClient = mcpClients.clients[selectedClientIndex]; DrawClientConfigurationCompact(selectedClient); } - + EditorGUILayout.Space(5); EditorGUILayout.EndVertical(); } @@ -582,10 +582,10 @@ private void AutoFirstRunSetup() MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}"); } } - lastClientRegisteredOk = anyRegistered - || IsCursorConfigured(pythonDir) - || CodexConfigHelper.IsCodexConfigured(pythonDir) - || IsClaudeConfigured(); + lastClientRegisteredOk = anyRegistered + || IsCursorConfigured(pythonDir) + || CodexConfigHelper.IsCodexConfigured(pythonDir) + || IsClaudeConfigured(); } // Ensure the bridge is listening and has a fresh saved port @@ -676,10 +676,10 @@ private void RunSetupNow() UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}"); } } - lastClientRegisteredOk = anyRegistered - || IsCursorConfigured(pythonDir) - || CodexConfigHelper.IsCodexConfigured(pythonDir) - || IsClaudeConfigured(); + lastClientRegisteredOk = anyRegistered + || IsCursorConfigured(pythonDir) + || CodexConfigHelper.IsCodexConfigured(pythonDir) + || IsClaudeConfigured(); // Restart/ensure bridge MCPForUnityBridge.StartAutoConnect(); @@ -695,14 +695,14 @@ private void RunSetupNow() } } - private static bool IsCursorConfigured(string pythonDir) - { - try - { - string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + private static bool IsCursorConfigured(string pythonDir) + { + try + { + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json") - : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"); if (!File.Exists(configPath)) return false; string json = File.ReadAllText(configPath); @@ -861,37 +861,37 @@ private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int max private void DrawClientConfigurationCompact(McpClient mcpClient) { - // Special pre-check for Claude Code: if CLI missing, reflect in status UI - if (mcpClient.mcpType == McpTypes.ClaudeCode) - { - string claudeCheck = ExecPath.ResolveClaude(); - if (string.IsNullOrEmpty(claudeCheck)) - { - mcpClient.configStatus = "Claude Not Found"; - mcpClient.status = McpStatus.NotConfigured; - } - } - - // Pre-check for clients that require uv (all except Claude Code) - bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode; - bool uvMissingEarly = false; - if (uvRequired) - { - string uvPathEarly = FindUvPath(); - if (string.IsNullOrEmpty(uvPathEarly)) - { - uvMissingEarly = true; - mcpClient.configStatus = "uv Not Found"; - mcpClient.status = McpStatus.NotConfigured; - } - } + // Special pre-check for Claude Code: if CLI missing, reflect in status UI + if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + string claudeCheck = ExecPath.ResolveClaude(); + if (string.IsNullOrEmpty(claudeCheck)) + { + mcpClient.configStatus = "Claude Not Found"; + mcpClient.status = McpStatus.NotConfigured; + } + } + + // Pre-check for clients that require uv (all except Claude Code) + bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode; + bool uvMissingEarly = false; + if (uvRequired) + { + string uvPathEarly = FindUvPath(); + if (string.IsNullOrEmpty(uvPathEarly)) + { + uvMissingEarly = true; + mcpClient.configStatus = "uv Not Found"; + mcpClient.status = McpStatus.NotConfigured; + } + } // Status display EditorGUILayout.BeginHorizontal(); Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); Color statusColor = GetStatusColor(mcpClient.status); DrawStatusDot(statusRect, statusColor, 16); - + GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label) { fontSize = 12, @@ -899,68 +899,68 @@ private void DrawClientConfigurationCompact(McpClient mcpClient) }; EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28)); EditorGUILayout.EndHorizontal(); - // When Claude CLI is missing, show a clear install hint directly below status - if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) - { - GUIStyle installHintStyle = new GUIStyle(clientStatusStyle); - installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange - EditorGUILayout.BeginHorizontal(); - GUIContent installText = new GUIContent("Make sure Claude Code is installed!"); - Vector2 textSize = installHintStyle.CalcSize(installText); - EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false)); - GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; - GUILayout.Space(6); - if (GUILayout.Button("[HELP]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false))) - { - Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code"); - } - EditorGUILayout.EndHorizontal(); - } - - EditorGUILayout.Space(10); - - // If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls - if (uvRequired && uvMissingEarly) - { - GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label) - { - fontSize = 12, - fontStyle = FontStyle.Bold, - wordWrap = false - }; - installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f); - EditorGUILayout.BeginHorizontal(); - GUIContent installText2 = new GUIContent("Make sure uv is installed!"); - Vector2 sz = installHintStyle2.CalcSize(installText2); - EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false)); - GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; - GUILayout.Space(6); - if (GUILayout.Button("[HELP]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false))) - { - Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf"); - } - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(8); - EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button("Choose uv Install Location", GUILayout.Width(260), GUILayout.Height(22))) - { - string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, ""); - if (!string.IsNullOrEmpty(picked)) - { - EditorPrefs.SetString("MCPForUnity.UvPath", picked); - ConfigureMcpClient(mcpClient); - Repaint(); - } - } - EditorGUILayout.EndHorizontal(); - return; - } - + // When Claude CLI is missing, show a clear install hint directly below status + if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) + { + GUIStyle installHintStyle = new GUIStyle(clientStatusStyle); + installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange + EditorGUILayout.BeginHorizontal(); + GUIContent installText = new GUIContent("Make sure Claude Code is installed!"); + Vector2 textSize = installHintStyle.CalcSize(installText); + EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false)); + GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; + GUILayout.Space(6); + if (GUILayout.Button("[HELP]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code"); + } + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(10); + + // If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls + if (uvRequired && uvMissingEarly) + { + GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold, + wordWrap = false + }; + installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f); + EditorGUILayout.BeginHorizontal(); + GUIContent installText2 = new GUIContent("Make sure uv is installed!"); + Vector2 sz = installHintStyle2.CalcSize(installText2); + EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false)); + GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; + GUILayout.Space(6); + if (GUILayout.Button("[HELP]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf"); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Choose uv Install Location", GUILayout.Width(260), GUILayout.Height(22))) + { + string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, ""); + if (!string.IsNullOrEmpty(picked)) + { + EditorPrefs.SetString("MCPForUnity.UvPath", picked); + ConfigureMcpClient(mcpClient); + Repaint(); + } + } + EditorGUILayout.EndHorizontal(); + return; + } + // Action buttons in horizontal layout EditorGUILayout.BeginHorizontal(); - + if (mcpClient.mcpType == McpTypes.VSCode) { if (GUILayout.Button("Auto Configure", GUILayout.Height(32))) @@ -968,57 +968,57 @@ private void DrawClientConfigurationCompact(McpClient mcpClient) ConfigureMcpClient(mcpClient); } } - else if (mcpClient.mcpType == McpTypes.ClaudeCode) - { - bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); - if (claudeAvailable) - { - bool isConfigured = mcpClient.status == McpStatus.Configured; - string buttonText = isConfigured ? "Unregister MCP for Unity with Claude Code" : "Register with Claude Code"; - if (GUILayout.Button(buttonText, GUILayout.Height(32))) - { - if (isConfigured) - { - UnregisterWithClaudeCode(); - } - else - { - string pythonDir = FindPackagePythonDirectory(); - RegisterWithClaudeCode(pythonDir); - } - } - // Hide the picker once a valid binary is available - EditorGUILayout.EndHorizontal(); - EditorGUILayout.BeginHorizontal(); - GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true }; - string resolvedClaude = ExecPath.ResolveClaude(); - EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle); - EditorGUILayout.EndHorizontal(); - EditorGUILayout.BeginHorizontal(); - } - // CLI picker row (only when not found) - EditorGUILayout.EndHorizontal(); - EditorGUILayout.BeginHorizontal(); - if (!claudeAvailable) - { - // Only show the picker button in not-found state (no redundant "not found" label) - if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22))) - { - string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, ""); - if (!string.IsNullOrEmpty(picked)) - { - ExecPath.SetClaudeCliPath(picked); - // Auto-register after setting a valid path - string pythonDir = FindPackagePythonDirectory(); - RegisterWithClaudeCode(pythonDir); - Repaint(); - } - } - } - EditorGUILayout.EndHorizontal(); - EditorGUILayout.BeginHorizontal(); - } + else if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); + if (claudeAvailable) + { + bool isConfigured = mcpClient.status == McpStatus.Configured; + string buttonText = isConfigured ? "Unregister MCP for Unity with Claude Code" : "Register with Claude Code"; + if (GUILayout.Button(buttonText, GUILayout.Height(32))) + { + if (isConfigured) + { + UnregisterWithClaudeCode(); + } + else + { + string pythonDir = FindPackagePythonDirectory(); + RegisterWithClaudeCode(pythonDir); + } + } + // Hide the picker once a valid binary is available + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true }; + string resolvedClaude = ExecPath.ResolveClaude(); + EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + } + // CLI picker row (only when not found) + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + if (!claudeAvailable) + { + // Only show the picker button in not-found state (no redundant "not found" label) + if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22))) + { + string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, ""); + if (!string.IsNullOrEmpty(picked)) + { + ExecPath.SetClaudeCliPath(picked); + // Auto-register after setting a valid path + string pythonDir = FindPackagePythonDirectory(); + RegisterWithClaudeCode(pythonDir); + Repaint(); + } + } + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + } else { if (GUILayout.Button($"Auto Configure", GUILayout.Height(32))) @@ -1026,7 +1026,7 @@ private void DrawClientConfigurationCompact(McpClient mcpClient) ConfigureMcpClient(mcpClient); } } - + if (mcpClient.mcpType != McpTypes.ClaudeCode) { if (GUILayout.Button("Manual Setup", GUILayout.Height(32))) @@ -1034,7 +1034,7 @@ private void DrawClientConfigurationCompact(McpClient mcpClient) string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath; - + if (mcpClient.mcpType == McpTypes.VSCode) { string pythonDir = FindPackagePythonDirectory(); @@ -1066,22 +1066,22 @@ private void DrawClientConfigurationCompact(McpClient mcpClient) } } } - + EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(8); - // Quick info (hide when Claude is not found to avoid confusion) - bool hideConfigInfo = - (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) - || ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath())); - if (!hideConfigInfo) - { - GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel) - { - fontSize = 10 - }; - EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle); - } + + EditorGUILayout.Space(8); + // Quick info (hide when Claude is not found to avoid confusion) + bool hideConfigInfo = + (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) + || ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath())); + if (!hideConfigInfo) + { + GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel) + { + fontSize = 10 + }; + EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle); + } } private void ToggleUnityBridge() @@ -1099,52 +1099,52 @@ private void ToggleUnityBridge() Repaint(); } - private static bool IsValidUv(string path) - { - return !string.IsNullOrEmpty(path) - && System.IO.Path.IsPathRooted(path) - && System.IO.File.Exists(path); - } - - private static bool ValidateUvBinarySafe(string path) - { - try - { - if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false; - var psi = new System.Diagnostics.ProcessStartInfo - { - FileName = path, - Arguments = "--version", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using var p = System.Diagnostics.Process.Start(psi); - if (p == null) return false; - if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; } - if (p.ExitCode != 0) return false; - string output = p.StandardOutput.ReadToEnd().Trim(); - return output.StartsWith("uv "); - } - catch { return false; } - } - - private static bool ArgsEqual(string[] a, string[] b) - { - if (a == null || b == null) return a == b; - if (a.Length != b.Length) return false; - for (int i = 0; i < a.Length; i++) - { - if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false; - } - return true; - } + private static bool IsValidUv(string path) + { + return !string.IsNullOrEmpty(path) + && System.IO.Path.IsPathRooted(path) + && System.IO.File.Exists(path); + } + + private static bool ValidateUvBinarySafe(string path) + { + try + { + if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false; + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = path, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = System.Diagnostics.Process.Start(psi); + if (p == null) return false; + if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; } + if (p.ExitCode != 0) return false; + string output = p.StandardOutput.ReadToEnd().Trim(); + return output.StartsWith("uv "); + } + catch { return false; } + } + + private static bool ArgsEqual(string[] a, string[] b) + { + if (a == null || b == null) return a == b; + if (a.Length != b.Length) return false; + for (int i = 0; i < a.Length; i++) + { + if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false; + } + return true; + } private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null) { - // 0) Respect explicit lock (hidden pref or UI toggle) - try { if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { } + // 0) Respect explicit lock (hidden pref or UI toggle) + try { if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { } JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; @@ -1185,52 +1185,52 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC existingConfig = new Newtonsoft.Json.Linq.JObject(); } - // Determine existing entry references (command/args) - string existingCommand = null; - string[] existingArgs = null; - bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode); - try - { - if (isVSCode) - { - existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); - existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject(); - } - else - { - existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); - existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject(); - } - } - catch { } - - // 1) Start from existing, only fill gaps (prefer trusted resolver) - string uvPath = ServerInstaller.FindUvPath(); - // Optionally trust existingCommand if it looks like uv/uv.exe - try - { - var name = System.IO.Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); - if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand)) - { - uvPath = existingCommand; - } - } - catch { } - if (uvPath == null) return "UV package manager not found. Please install UV first."; - string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); - - // 2) Canonical args order - var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; - - // 3) Only write if changed - bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) - || !ArgsEqual(existingArgs, newArgs); - if (!changed) - { - return "Configured successfully"; // nothing to do - } - - // 4) Ensure containers exist and write back minimal changes + // Determine existing entry references (command/args) + string existingCommand = null; + string[] existingArgs = null; + bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode); + try + { + if (isVSCode) + { + existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); + existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject(); + } + else + { + existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); + existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject(); + } + } + catch { } + + // 1) Start from existing, only fill gaps (prefer trusted resolver) + string uvPath = ServerInstaller.FindUvPath(); + // Optionally trust existingCommand if it looks like uv/uv.exe + try + { + var name = System.IO.Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand)) + { + uvPath = existingCommand; + } + } + catch { } + if (uvPath == null) return "UV package manager not found. Please install UV first."; + string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + + // 2) Canonical args order + var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; + + // 3) Only write if changed + bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) + || !ArgsEqual(existingArgs, newArgs); + if (!changed) + { + return "Configured successfully"; // nothing to do + } + + // 4) Ensure containers exist and write back minimal changes JObject existingRoot; if (existingConfig is JObject eo) existingRoot = eo; @@ -1239,18 +1239,18 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient); - string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); - - McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); + string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); - try - { - if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); - UnityEditor.EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); - } - catch { } + McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); + + try + { + if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); + UnityEditor.EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); + } + catch { } - return "Configured successfully"; + return "Configured successfully"; } private void ShowManualConfigurationInstructions( @@ -1264,23 +1264,23 @@ McpClient mcpClient } // New method to show manual instructions without changing status - private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient) - { - // Get the Python directory path using Package Manager API - string pythonDir = FindPackagePythonDirectory(); - // Build manual JSON centrally using the shared builder - string uvPathForManual = FindUvPath(); - if (uvPathForManual == null) - { - UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration."); - return; - } - - string manualConfig = mcpClient?.mcpType == McpTypes.Codex - ? CodexConfigHelper.BuildCodexServerBlock(uvPathForManual, McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)).TrimEnd() + Environment.NewLine - : ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient); - ManualConfigEditorWindow.ShowWindow(configPath, manualConfig, mcpClient); - } + private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient) + { + // Get the Python directory path using Package Manager API + string pythonDir = FindPackagePythonDirectory(); + // Build manual JSON centrally using the shared builder + string uvPathForManual = FindUvPath(); + if (uvPathForManual == null) + { + UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration."); + return; + } + + string manualConfig = mcpClient?.mcpType == McpTypes.Codex + ? CodexConfigHelper.BuildCodexServerBlock(uvPathForManual, McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)).TrimEnd() + Environment.NewLine + : ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient); + ManualConfigEditorWindow.ShowWindow(configPath, manualConfig, mcpClient); + } private string FindPackagePythonDirectory() { @@ -1297,7 +1297,7 @@ private string FindPackagePythonDirectory() Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"), Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"), }; - + foreach (string devPath in devPaths) { if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py"))) @@ -1311,25 +1311,25 @@ private string FindPackagePythonDirectory() } } - // Resolve via shared helper (handles local registry and older fallback) only if dev override on - if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false)) - { - if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) - { - return embedded; - } - } - - // Log only if the resolved path does not actually contain server.py - if (debugLogsEnabled) - { - bool hasServer = false; - try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { } - if (!hasServer) - { - UnityEngine.Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path"); - } - } + // Resolve via shared helper (handles local registry and older fallback) only if dev override on + if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false)) + { + if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) + { + return embedded; + } + } + + // Log only if the resolved path does not actually contain server.py + if (debugLogsEnabled) + { + bool hasServer = false; + try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { } + if (!hasServer) + { + UnityEngine.Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path"); + } + } } catch (Exception e) { @@ -1368,12 +1368,12 @@ private bool IsDevelopmentMode() } } - private string ConfigureMcpClient(McpClient mcpClient) - { - try - { - // Determine the config file path based on OS - string configPath; + private string ConfigureMcpClient(McpClient mcpClient) + { + try + { + // Determine the config file path based on OS + string configPath; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -1401,23 +1401,23 @@ private string ConfigureMcpClient(McpClient mcpClient) // Create directory if it doesn't exist Directory.CreateDirectory(Path.GetDirectoryName(configPath)); - // Find the server.py file location using the same logic as FindPackagePythonDirectory - string pythonDir = FindPackagePythonDirectory(); + // Find the server.py file location using the same logic as FindPackagePythonDirectory + string pythonDir = FindPackagePythonDirectory(); - if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) - { - ShowManualInstructionsWindow(configPath, mcpClient); - return "Manual Configuration Required"; - } + if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) + { + ShowManualInstructionsWindow(configPath, mcpClient); + return "Manual Configuration Required"; + } - string result = mcpClient.mcpType == McpTypes.Codex - ? ConfigureCodexClient(pythonDir, configPath, mcpClient) - : WriteToConfig(pythonDir, configPath, mcpClient); + string result = mcpClient.mcpType == McpTypes.Codex + ? ConfigureCodexClient(pythonDir, configPath, mcpClient) + : WriteToConfig(pythonDir, configPath, mcpClient); - // Update the client status after successful configuration - if (result == "Configured successfully") - { - mcpClient.SetStatus(McpStatus.Configured); + // Update the client status after successful configuration + if (result == "Configured successfully") + { + mcpClient.SetStatus(McpStatus.Configured); } return result; @@ -1450,82 +1450,82 @@ private string ConfigureMcpClient(McpClient mcpClient) $"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}" ); return $"Failed to configure {mcpClient.name}"; - } - } - - private string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient) - { - try { if (EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { } - - string existingToml = string.Empty; - if (File.Exists(configPath)) - { - try - { - existingToml = File.ReadAllText(configPath); - } - catch (Exception e) - { - if (debugLogsEnabled) - { - UnityEngine.Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}"); - } - existingToml = string.Empty; - } - } - - string existingCommand = null; - string[] existingArgs = null; - if (!string.IsNullOrWhiteSpace(existingToml)) - { - CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); - } - - string uvPath = ServerInstaller.FindUvPath(); - try - { - var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); - if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand)) - { - uvPath = existingCommand; - } - } - catch { } - - if (uvPath == null) - { - return "UV package manager not found. Please install UV first."; - } - - string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); - var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; - - bool changed = true; - if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null) - { - changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) - || !ArgsEqual(existingArgs, newArgs); - } - - if (!changed) - { - return "Configured successfully"; - } - - string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc); - string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock); - - McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml); - - try - { - if (IsValidUv(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); - EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); - } - catch { } - - return "Configured successfully"; - } + } + } + + private string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient) + { + try { if (EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { } + + string existingToml = string.Empty; + if (File.Exists(configPath)) + { + try + { + existingToml = File.ReadAllText(configPath); + } + catch (Exception e) + { + if (debugLogsEnabled) + { + UnityEngine.Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}"); + } + existingToml = string.Empty; + } + } + + string existingCommand = null; + string[] existingArgs = null; + if (!string.IsNullOrWhiteSpace(existingToml)) + { + CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); + } + + string uvPath = ServerInstaller.FindUvPath(); + try + { + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand)) + { + uvPath = existingCommand; + } + } + catch { } + + if (uvPath == null) + { + return "UV package manager not found. Please install UV first."; + } + + string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; + + bool changed = true; + if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null) + { + changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) + || !ArgsEqual(existingArgs, newArgs); + } + + if (!changed) + { + return "Configured successfully"; + } + + string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc); + string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock); + + McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml); + + try + { + if (IsValidUv(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); + EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); + } + catch { } + + return "Configured successfully"; + } private void ShowCursorManualConfigurationInstructions( string configPath, @@ -1544,7 +1544,7 @@ McpClient mcpClient UnityEngine.Debug.LogError("UV package manager not found. Cannot configure manual setup."); return; } - + McpConfig jsonConfig = new() { mcpServers = new McpConfigServers @@ -1617,7 +1617,7 @@ private void CheckMcpConfiguration(McpClient mcpClient) CheckClaudeCodeConfiguration(mcpClient); return; } - + string configPath; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -1652,42 +1652,42 @@ private void CheckMcpConfiguration(McpClient mcpClient) string configJson = File.ReadAllText(configPath); // Use the same path resolution as configuration to avoid false "Incorrect Path" in dev mode string pythonDir = FindPackagePythonDirectory(); - + // Use switch statement to handle different client types, extracting common logic string[] args = null; bool configExists = false; - - switch (mcpClient.mcpType) - { - case McpTypes.VSCode: - dynamic config = JsonConvert.DeserializeObject(configJson); - - // New schema: top-level servers - if (config?.servers?.unityMCP != null) - { - args = config.servers.unityMCP.args.ToObject(); - configExists = true; - } - // Back-compat: legacy mcp.servers - else if (config?.mcp?.servers?.unityMCP != null) - { - args = config.mcp.servers.unityMCP.args.ToObject(); - configExists = true; - } - break; - - case McpTypes.Codex: - if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs)) - { - args = codexArgs; - configExists = true; - } - break; - - default: - // Standard MCP configuration check for Claude Desktop, Cursor, etc. - McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); - + + switch (mcpClient.mcpType) + { + case McpTypes.VSCode: + dynamic config = JsonConvert.DeserializeObject(configJson); + + // New schema: top-level servers + if (config?.servers?.unityMCP != null) + { + args = config.servers.unityMCP.args.ToObject(); + configExists = true; + } + // Back-compat: legacy mcp.servers + else if (config?.mcp?.servers?.unityMCP != null) + { + args = config.mcp.servers.unityMCP.args.ToObject(); + configExists = true; + } + break; + + case McpTypes.Codex: + if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs)) + { + args = codexArgs; + configExists = true; + } + break; + + default: + // Standard MCP configuration check for Claude Desktop, Cursor, etc. + McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); + if (standardConfig?.mcpServers?.unityMCP != null) { args = standardConfig.mcpServers.unityMCP.args; @@ -1695,7 +1695,7 @@ private void CheckMcpConfiguration(McpClient mcpClient) } break; } - + // Common logic for checking configuration status if (configExists) { @@ -1812,35 +1812,35 @@ private void UnregisterWithClaudeCode() ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" : null; // On Windows, don't modify PATH - use system PATH as-is - // Determine if Claude has a "UnityMCP" server registered by using exit codes from `claude mcp get ` - string[] candidateNamesForGet = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" }; - List existingNames = new List(); - foreach (var candidate in candidateNamesForGet) - { - if (ExecPath.TryRun(claudePath, $"mcp get {candidate}", projectDir, out var getStdout, out var getStderr, 7000, pathPrepend)) - { - // Success exit code indicates the server exists - existingNames.Add(candidate); - } - } - - if (existingNames.Count == 0) - { - // Nothing to unregister – set status and bail early - var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); - if (claudeClient != null) - { - claudeClient.SetStatus(McpStatus.NotConfigured); - UnityEngine.Debug.Log("Claude CLI reports no MCP for Unity server via 'mcp get' - setting status to NotConfigured and aborting unregister."); - Repaint(); - } - return; - } - + // Determine if Claude has a "UnityMCP" server registered by using exit codes from `claude mcp get ` + string[] candidateNamesForGet = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" }; + List existingNames = new List(); + foreach (var candidate in candidateNamesForGet) + { + if (ExecPath.TryRun(claudePath, $"mcp get {candidate}", projectDir, out var getStdout, out var getStderr, 7000, pathPrepend)) + { + // Success exit code indicates the server exists + existingNames.Add(candidate); + } + } + + if (existingNames.Count == 0) + { + // Nothing to unregister – set status and bail early + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + claudeClient.SetStatus(McpStatus.NotConfigured); + UnityEngine.Debug.Log("Claude CLI reports no MCP for Unity server via 'mcp get' - setting status to NotConfigured and aborting unregister."); + Repaint(); + } + return; + } + // Try different possible server names string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" }; bool success = false; - + foreach (string serverName in possibleNames) { if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend)) @@ -1905,7 +1905,7 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient) // Get the Unity project directory to check project-specific config string unityProjectDir = Application.dataPath; string projectDir = Path.GetDirectoryName(unityProjectDir); - + // Read the global Claude config file (honor macConfigPath on macOS) string configPath; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -1914,22 +1914,22 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient) configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) ? mcpClient.linuxConfigPath : mcpClient.macConfigPath; else configPath = mcpClient.linuxConfigPath; - + if (debugLogsEnabled) { MCPForUnity.Editor.Helpers.McpLog.Info($"Checking Claude config at: {configPath}", always: false); } - + if (!File.Exists(configPath)) { UnityEngine.Debug.LogWarning($"Claude config file not found at: {configPath}"); mcpClient.SetStatus(McpStatus.NotConfigured); return; } - + string configJson = File.ReadAllText(configPath); dynamic claudeConfig = JsonConvert.DeserializeObject(configJson); - + // Check for "UnityMCP" server in the mcpServers section (current format) if (claudeConfig?.mcpServers != null) { @@ -1941,7 +1941,7 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient) return; } } - + // Also check if there's a project-specific configuration for this Unity project (legacy format) if (claudeConfig?.projects != null) { @@ -1949,11 +1949,11 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient) foreach (var project in claudeConfig.projects) { string projectPath = project.Name; - + // Normalize paths for comparison (handle forward/back slash differences) string normalizedProjectPath = Path.GetFullPath(projectPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string normalizedProjectDir = Path.GetFullPath(projectDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - + if (string.Equals(normalizedProjectPath, normalizedProjectDir, StringComparison.OrdinalIgnoreCase) && project.Value?.mcpServers != null) { // Check for "UnityMCP" (case variations) @@ -1967,7 +1967,7 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient) } } } - + // No configuration found for this project mcpClient.SetStatus(McpStatus.NotConfigured); } @@ -2004,7 +2004,7 @@ private bool IsPythonDetected() Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\python.exe"), }; - + foreach (string c in windowsCandidates) { if (File.Exists(c)) return true; diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs index e5544510..10e066d2 100644 --- a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs @@ -13,14 +13,14 @@ public static void ShowWindow(string configPath, string configJson) window.configPath = configPath; window.configJson = configJson; window.minSize = new Vector2(550, 500); - + // Create a McpClient for VSCode window.mcpClient = new McpClient { name = "VSCode GitHub Copilot", mcpType = McpTypes.VSCode }; - + window.Show(); } @@ -84,7 +84,7 @@ protected override void OnGUI() instructionStyle ); EditorGUILayout.Space(5); - + EditorGUILayout.LabelField( "2. Steps to Configure", EditorStyles.boldLabel @@ -102,7 +102,7 @@ protected override void OnGUI() instructionStyle ); EditorGUILayout.Space(5); - + EditorGUILayout.LabelField( "3. VSCode mcp.json location:", EditorStyles.boldLabel @@ -120,7 +120,7 @@ protected override void OnGUI() "mcp.json" ); } - else + else { displayPath = System.IO.Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), diff --git a/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs b/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs index 05503f42..c76b280d 100644 --- a/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs +++ b/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs @@ -110,7 +110,7 @@ public override Color ReadJson(JsonReader reader, Type objectType, Color existin ); } } - + public class RectConverter : JsonConverter { public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer) @@ -138,7 +138,7 @@ public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingV ); } } - + public class BoundsConverter : JsonConverter { public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer) @@ -263,4 +263,4 @@ public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object"); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer~/src/server.py b/UnityMcpBridge/UnityMcpServer~/src/server.py index a2765cc2..af6fe036 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/server.py +++ b/UnityMcpBridge/UnityMcpServer~/src/server.py @@ -1,3 +1,4 @@ +from telemetry import record_telemetry, record_milestone, RecordType, MilestoneType from mcp.server.fastmcp import FastMCP import logging from logging.handlers import RotatingFileHandler @@ -21,10 +22,12 @@ # Also write logs to a rotating file so logs are available when launched via stdio try: import os as _os - _log_dir = _os.path.join(_os.path.expanduser("~/Library/Application Support/UnityMCP"), "Logs") + _log_dir = _os.path.join(_os.path.expanduser( + "~/Library/Application Support/UnityMCP"), "Logs") _os.makedirs(_log_dir, exist_ok=True) _file_path = _os.path.join(_log_dir, "unity_mcp_server.log") - _fh = RotatingFileHandler(_file_path, maxBytes=512*1024, backupCount=2, encoding="utf-8") + _fh = RotatingFileHandler( + _file_path, maxBytes=512*1024, backupCount=2, encoding="utf-8") _fh.setFormatter(logging.Formatter(config.log_format)) _fh.setLevel(getattr(logging, config.log_level)) logger.addHandler(_fh) @@ -42,7 +45,8 @@ # Quieten noisy third-party loggers to avoid clutter during stdio handshake for noisy in ("httpx", "urllib3"): try: - logging.getLogger(noisy).setLevel(max(logging.WARNING, getattr(logging, config.log_level))) + logging.getLogger(noisy).setLevel( + max(logging.WARNING, getattr(logging, config.log_level))) except Exception: pass @@ -50,13 +54,11 @@ # Ensure a slightly higher telemetry timeout unless explicitly overridden by env try: - # Ensure generous timeout unless explicitly overridden by env if not os.environ.get("UNITY_MCP_TELEMETRY_TIMEOUT"): os.environ["UNITY_MCP_TELEMETRY_TIMEOUT"] = "5.0" except Exception: pass -from telemetry import record_telemetry, record_milestone, RecordType, MilestoneType # Global connection state _unity_connection: UnityConnection = None @@ -67,7 +69,7 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: """Handle server startup and shutdown.""" global _unity_connection logger.info("MCP for Unity Server starting up") - + # Record server startup telemetry start_time = time.time() start_clk = time.perf_counter() @@ -79,6 +81,7 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: server_version = "unknown" # Defer initial telemetry by 1s to avoid stdio handshake interference import threading + def _emit_startup(): try: record_telemetry(RecordType.STARTUP, { @@ -89,15 +92,17 @@ def _emit_startup(): except Exception: logger.debug("Deferred startup telemetry failed", exc_info=True) threading.Timer(1.0, _emit_startup).start() - + try: - skip_connect = os.environ.get("UNITY_MCP_SKIP_STARTUP_CONNECT", "").lower() in ("1", "true", "yes", "on") + skip_connect = os.environ.get( + "UNITY_MCP_SKIP_STARTUP_CONNECT", "").lower() in ("1", "true", "yes", "on") if skip_connect: - logger.info("Skipping Unity connection on startup (UNITY_MCP_SKIP_STARTUP_CONNECT=1)") + logger.info( + "Skipping Unity connection on startup (UNITY_MCP_SKIP_STARTUP_CONNECT=1)") else: _unity_connection = get_unity_connection() logger.info("Connected to Unity on startup") - + # Record successful Unity connection (deferred) import threading as _t _t.Timer(1.0, lambda: record_telemetry( @@ -107,11 +112,11 @@ def _emit_startup(): "connection_time_ms": (time.perf_counter() - start_clk) * 1000, } )).start() - + except ConnectionError as e: logger.warning("Could not connect to Unity on startup: %s", e) _unity_connection = None - + # Record connection failure (deferred) import threading as _t _err_msg = str(e)[:200] @@ -124,7 +129,8 @@ def _emit_startup(): } )).start() except Exception as e: - logger.warning("Unexpected error connecting to Unity on startup: %s", e) + logger.warning( + "Unexpected error connecting to Unity on startup: %s", e) _unity_connection = None import threading as _t _err_msg = str(e)[:200] @@ -136,7 +142,7 @@ def _emit_startup(): "connection_time_ms": (time.perf_counter() - start_clk) * 1000, } )).start() - + try: # Yield the connection object so it can be attached to the context # The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge) diff --git a/mcp_source.py b/mcp_source.py index 7d5a48a3..203dbe75 100755 --- a/mcp_source.py +++ b/mcp_source.py @@ -32,7 +32,8 @@ def run_git(repo: pathlib.Path, *args: str) -> str: "git", "-C", str(repo), *args ], capture_output=True, text=True) if result.returncode != 0: - raise RuntimeError(result.stderr.strip() or f"git {' '.join(args)} failed") + raise RuntimeError(result.stderr.strip() + or f"git {' '.join(args)} failed") return result.stdout.strip() @@ -77,7 +78,8 @@ def find_manifest(explicit: Optional[str]) -> pathlib.Path: candidate = parent / "Packages" / "manifest.json" if candidate.exists(): return candidate - raise FileNotFoundError("Could not find Packages/manifest.json from current directory. Use --manifest to specify a path.") + raise FileNotFoundError( + "Could not find Packages/manifest.json from current directory. Use --manifest to specify a path.") def read_json(path: pathlib.Path) -> dict: @@ -103,16 +105,21 @@ def build_options(repo_root: pathlib.Path, branch: str, origin_https: str): origin_remote = origin return [ ("[1] Upstream main", upstream), - ("[2] Remote current branch", f"{origin_remote}?path=/{BRIDGE_SUBPATH}#{branch}"), - ("[3] Local workspace", f"file:{(repo_root / BRIDGE_SUBPATH).as_posix()}"), + ("[2] Remote current branch", + f"{origin_remote}?path=/{BRIDGE_SUBPATH}#{branch}"), + ("[3] Local workspace", + f"file:{(repo_root / BRIDGE_SUBPATH).as_posix()}"), ] def parse_args() -> argparse.Namespace: - p = argparse.ArgumentParser(description="Switch MCP for Unity package source") + p = argparse.ArgumentParser( + description="Switch MCP for Unity package source") p.add_argument("--manifest", help="Path to Packages/manifest.json") - p.add_argument("--repo", help="Path to unity-mcp repo root (for local file option)") - p.add_argument("--choice", choices=["1", "2", "3"], help="Pick option non-interactively") + p.add_argument( + "--repo", help="Path to unity-mcp repo root (for local file option)") + p.add_argument( + "--choice", choices=["1", "2", "3"], help="Pick option non-interactively") return p.parse_args() @@ -153,7 +160,8 @@ def main() -> None: data = read_json(manifest_path) deps = data.get("dependencies", {}) if PKG_NAME not in deps: - print(f"Error: '{PKG_NAME}' not found in manifest dependencies.", file=sys.stderr) + print( + f"Error: '{PKG_NAME}' not found in manifest dependencies.", file=sys.stderr) sys.exit(1) print(f"\nUpdating {PKG_NAME} → {chosen}") diff --git a/test_unity_socket_framing.py b/test_unity_socket_framing.py index 7c0cb93f..27d36855 100644 --- a/test_unity_socket_framing.py +++ b/test_unity_socket_framing.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 -import socket, struct, json, sys +import socket +import struct +import json +import sys HOST = "127.0.0.1" PORT = 6400 @@ -10,6 +13,7 @@ FILL = "R" MAX_FRAME = 64 * 1024 * 1024 + def recv_exact(sock, n): buf = bytearray(n) view = memoryview(buf) @@ -21,6 +25,7 @@ def recv_exact(sock, n): off += r return bytes(buf) + def is_valid_json(b): try: json.loads(b.decode("utf-8")) @@ -28,6 +33,7 @@ def is_valid_json(b): except Exception: return False + def recv_legacy_json(sock, timeout=60): sock.settimeout(timeout) chunks = [] @@ -45,6 +51,7 @@ def recv_legacy_json(sock, timeout=60): if is_valid_json(data): return data + def main(): # Cap filler to stay within framing limit (reserve small overhead for JSON) safe_max = max(1, MAX_FRAME - 4096) @@ -83,16 +90,16 @@ def main(): print(f"Response framed length: {resp_len}") MAX_RESP = MAX_FRAME if resp_len <= 0 or resp_len > MAX_RESP: - raise RuntimeError(f"invalid framed length: {resp_len} (max {MAX_RESP})") + raise RuntimeError( + f"invalid framed length: {resp_len} (max {MAX_RESP})") resp = recv_exact(s, resp_len) else: s.sendall(body_bytes) resp = recv_legacy_json(s) print(f"Response bytes: {len(resp)}") - print(f"Response head: {resp[:120].decode('utf-8','ignore')}") + print(f"Response head: {resp[:120].decode('utf-8', 'ignore')}") + if __name__ == "__main__": main() - - diff --git a/tests/conftest.py b/tests/conftest.py index a839e9c4..fede9707 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,4 +5,3 @@ os.environ.setdefault("DISABLE_TELEMETRY", "true") os.environ.setdefault("UNITY_MCP_DISABLE_TELEMETRY", "true") os.environ.setdefault("MCP_DISABLE_TELEMETRY", "true") - diff --git a/tests/test_edit_normalization_and_noop.py b/tests/test_edit_normalization_and_noop.py index ab97e5e2..86f60afa 100644 --- a/tests/test_edit_normalization_and_noop.py +++ b/tests/test_edit_normalization_and_noop.py @@ -12,7 +12,12 @@ mcp_pkg = types.ModuleType("mcp") server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") -class _Dummy: pass + + +class _Dummy: + pass + + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -21,22 +26,27 @@ class _Dummy: pass sys.modules.setdefault("mcp.server", server_pkg) sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) + def _load(path: pathlib.Path, name: str): spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod + manage_script = _load(SRC / "tools" / "manage_script.py", "manage_script_mod2") -manage_script_edits = _load(SRC / "tools" / "manage_script_edits.py", "manage_script_edits_mod2") +manage_script_edits = _load( + SRC / "tools" / "manage_script_edits.py", "manage_script_edits_mod2") class DummyMCP: def __init__(self): self.tools = {} + def tool(self, *args, **kwargs): def deco(fn): self.tools[fn.__name__] = fn; return fn return deco + def setup_tools(): mcp = DummyMCP() manage_script.register_manage_script_tools(mcp) @@ -59,7 +69,8 @@ def fake_send(cmd, params): "range": {"start": {"line": 10, "character": 2}, "end": {"line": 10, "character": 2}}, "newText": "// lsp\n" }] - apply(None, uri="unity://path/Assets/Scripts/F.cs", edits=edits, precondition_sha256="x") + apply(None, uri="unity://path/Assets/Scripts/F.cs", + edits=edits, precondition_sha256="x") p = calls[-1] e = p["edits"][0] assert e["startLine"] == 11 and e["startCol"] == 3 @@ -68,24 +79,28 @@ def fake_send(cmd, params): calls.clear() edits = [{"range": [0, 0], "text": "// idx\n"}] # fake read to provide contents length + def fake_read(cmd, params): if params.get("action") == "read": return {"success": True, "data": {"contents": "hello\n"}} return {"success": True} monkeypatch.setattr(manage_script, "send_command_with_retry", fake_read) - apply(None, uri="unity://path/Assets/Scripts/F.cs", edits=edits, precondition_sha256="x") + apply(None, uri="unity://path/Assets/Scripts/F.cs", + edits=edits, precondition_sha256="x") # last call is apply_text_edits - + def test_noop_evidence_shape(monkeypatch): tools = setup_tools() apply = tools["apply_text_edits"] # Route response from Unity indicating no-op + def fake_send(cmd, params): return {"success": True, "data": {"no_op": True, "evidence": {"reason": "identical_content"}}} monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send) - resp = apply(None, uri="unity://path/Assets/Scripts/F.cs", edits=[{"startLine":1,"startCol":1,"endLine":1,"endCol":1,"newText":""}], precondition_sha256="x") + resp = apply(None, uri="unity://path/Assets/Scripts/F.cs", edits=[ + {"startLine": 1, "startCol": 1, "endLine": 1, "endCol": 1, "newText": ""}], precondition_sha256="x") assert resp["success"] is True assert resp.get("data", {}).get("no_op") is True @@ -93,9 +108,11 @@ def fake_send(cmd, params): def test_atomic_multi_span_and_relaxed(monkeypatch): tools_text = setup_tools() apply_text = tools_text["apply_text_edits"] - tools_struct = DummyMCP(); manage_script_edits.register_manage_script_edits_tools(tools_struct) + tools_struct = DummyMCP() + manage_script_edits.register_manage_script_edits_tools(tools_struct) # Fake send for read and write; verify atomic applyMode and validate=relaxed passes through sent = {} + def fake_send(cmd, params): if params.get("action") == "read": return {"success": True, "data": {"contents": "public class C{\nvoid M(){ int x=2; }\n}\n"}} @@ -105,12 +122,13 @@ def fake_send(cmd, params): edits = [ {"startLine": 2, "startCol": 14, "endLine": 2, "endCol": 15, "newText": "3"}, - {"startLine": 3, "startCol": 2, "endLine": 3, "endCol": 2, "newText": "// tail\n"} + {"startLine": 3, "startCol": 2, "endLine": 3, + "endCol": 2, "newText": "// tail\n"} ] - resp = apply_text(None, uri="unity://path/Assets/Scripts/C.cs", edits=edits, precondition_sha256="sha", options={"validate": "relaxed", "applyMode": "atomic"}) + resp = apply_text(None, uri="unity://path/Assets/Scripts/C.cs", edits=edits, + precondition_sha256="sha", options={"validate": "relaxed", "applyMode": "atomic"}) assert resp["success"] is True # Last manage_script call should include options with applyMode atomic and validate relaxed last = sent["calls"][-1] assert last.get("options", {}).get("applyMode") == "atomic" assert last.get("options", {}).get("validate") == "relaxed" - diff --git a/tests/test_edit_strict_and_warnings.py b/tests/test_edit_strict_and_warnings.py index 1d35323f..8ae3bdb6 100644 --- a/tests/test_edit_strict_and_warnings.py +++ b/tests/test_edit_strict_and_warnings.py @@ -12,7 +12,12 @@ mcp_pkg = types.ModuleType("mcp") server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") -class _Dummy: pass + + +class _Dummy: + pass + + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -34,6 +39,7 @@ def _load(path: pathlib.Path, name: str): class DummyMCP: def __init__(self): self.tools = {} + def tool(self, *args, **kwargs): def deco(fn): self.tools[fn.__name__] = fn; return fn return deco @@ -56,13 +62,16 @@ def fake_send(cmd, params): monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send) # Explicit fields given as 0-based (invalid); SDK should normalize and warn - edits = [{"startLine": 0, "startCol": 0, "endLine": 0, "endCol": 0, "newText": "//x"}] - resp = apply_edits(None, uri="unity://path/Assets/Scripts/F.cs", edits=edits, precondition_sha256="sha") + edits = [{"startLine": 0, "startCol": 0, + "endLine": 0, "endCol": 0, "newText": "//x"}] + resp = apply_edits(None, uri="unity://path/Assets/Scripts/F.cs", + edits=edits, precondition_sha256="sha") assert resp["success"] is True data = resp.get("data", {}) assert "normalizedEdits" in data - assert any(w == "zero_based_explicit_fields_normalized" for w in data.get("warnings", [])) + assert any( + w == "zero_based_explicit_fields_normalized" for w in data.get("warnings", [])) ne = data["normalizedEdits"][0] assert ne["startLine"] == 1 and ne["startCol"] == 1 and ne["endLine"] == 1 and ne["endCol"] == 1 @@ -76,9 +85,9 @@ def fake_send(cmd, params): monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send) - edits = [{"startLine": 0, "startCol": 0, "endLine": 0, "endCol": 0, "newText": "//x"}] - resp = apply_edits(None, uri="unity://path/Assets/Scripts/F.cs", edits=edits, precondition_sha256="sha", strict=True) + edits = [{"startLine": 0, "startCol": 0, + "endLine": 0, "endCol": 0, "newText": "//x"}] + resp = apply_edits(None, uri="unity://path/Assets/Scripts/F.cs", + edits=edits, precondition_sha256="sha", strict=True) assert resp["success"] is False assert resp.get("code") == "zero_based_explicit_fields" - - diff --git a/tests/test_find_in_file_minimal.py b/tests/test_find_in_file_minimal.py index 91e61ad3..a0ebd3b3 100644 --- a/tests/test_find_in_file_minimal.py +++ b/tests/test_find_in_file_minimal.py @@ -1,3 +1,4 @@ +from tools.resource_tools import register_resource_tools # type: ignore import sys import pathlib import importlib.util @@ -9,7 +10,6 @@ SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) -from tools.resource_tools import register_resource_tools # type: ignore class DummyMCP: def __init__(self): @@ -21,12 +21,14 @@ def deco(fn): return fn return deco + @pytest.fixture() def resource_tools(): mcp = DummyMCP() register_resource_tools(mcp) return mcp.tools + def test_find_in_file_returns_positions(resource_tools, tmp_path): proj = tmp_path assets = proj / "Assets" @@ -37,9 +39,11 @@ def test_find_in_file_returns_positions(resource_tools, tmp_path): loop = asyncio.new_event_loop() try: resp = loop.run_until_complete( - find_in_file(uri="unity://path/Assets/A.txt", pattern="world", ctx=None, project_root=str(proj)) + find_in_file(uri="unity://path/Assets/A.txt", + pattern="world", ctx=None, project_root=str(proj)) ) finally: loop.close() assert resp["success"] is True - assert resp["data"]["matches"] == [{"startLine": 1, "startCol": 7, "endLine": 1, "endCol": 12}] + assert resp["data"]["matches"] == [ + {"startLine": 1, "startCol": 7, "endLine": 1, "endCol": 12}] diff --git a/tests/test_get_sha.py b/tests/test_get_sha.py index 42bebaba..f0a0d7fa 100644 --- a/tests/test_get_sha.py +++ b/tests/test_get_sha.py @@ -13,9 +13,11 @@ server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") + class _Dummy: pass + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -32,7 +34,8 @@ def _load_module(path: pathlib.Path, name: str): return mod -manage_script = _load_module(SRC / "tools" / "manage_script.py", "manage_script_mod") +manage_script = _load_module( + SRC / "tools" / "manage_script.py", "manage_script_mod") class DummyMCP: @@ -72,4 +75,3 @@ def fake_send(cmd, params): assert captured["params"]["path"].endswith("Assets/Scripts") assert resp["success"] is True assert resp["data"] == {"sha256": "abc", "lengthBytes": 1} - diff --git a/tests/test_improved_anchor_matching.py b/tests/test_improved_anchor_matching.py index 5fd7c933..b3047c92 100644 --- a/tests/test_improved_anchor_matching.py +++ b/tests/test_improved_anchor_matching.py @@ -17,9 +17,11 @@ server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") + class _Dummy: pass + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -28,17 +30,21 @@ class _Dummy: sys.modules.setdefault("mcp.server", server_pkg) sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) + def load_module(path, name): spec = importlib.util.spec_from_file_location(name, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module -manage_script_edits_module = load_module(SRC / "tools" / "manage_script_edits.py", "manage_script_edits_module") + +manage_script_edits_module = load_module( + SRC / "tools" / "manage_script_edits.py", "manage_script_edits_module") + def test_improved_anchor_matching(): """Test that our improved anchor matching finds the right closing brace.""" - + test_code = '''using UnityEngine; public class TestClass : MonoBehaviour @@ -53,27 +59,29 @@ def test_improved_anchor_matching(): // Update logic } }''' - + import re - + # Test the problematic anchor pattern anchor_pattern = r"\s*}\s*$" flags = re.MULTILINE - + # Test our improved function best_match = manage_script_edits_module._find_best_anchor_match( anchor_pattern, test_code, flags, prefer_last=True ) - + assert best_match is not None, "anchor pattern not found" match_pos = best_match.start() line_num = test_code[:match_pos].count('\n') + 1 total_lines = test_code.count('\n') + 1 - assert line_num >= total_lines - 2, f"expected match near end (>= {total_lines-2}), got line {line_num}" + assert line_num >= total_lines - \ + 2, f"expected match near end (>= {total_lines-2}), got line {line_num}" + def test_old_vs_new_matching(): """Compare old vs new matching behavior.""" - + test_code = '''using UnityEngine; public class TestClass : MonoBehaviour @@ -96,30 +104,34 @@ def test_old_vs_new_matching(): // More logic } }''' - + import re - + anchor_pattern = r"\s*}\s*$" flags = re.MULTILINE - + # Old behavior (first match) old_match = re.search(anchor_pattern, test_code, flags) - old_line = test_code[:old_match.start()].count('\n') + 1 if old_match else None - + old_line = test_code[:old_match.start()].count( + '\n') + 1 if old_match else None + # New behavior (improved matching) new_match = manage_script_edits_module._find_best_anchor_match( anchor_pattern, test_code, flags, prefer_last=True ) - new_line = test_code[:new_match.start()].count('\n') + 1 if new_match else None - + new_line = test_code[:new_match.start()].count( + '\n') + 1 if new_match else None + assert old_line is not None and new_line is not None, "failed to locate anchors" assert new_line > old_line, f"improved matcher should choose a later line (old={old_line}, new={new_line})" total_lines = test_code.count('\n') + 1 - assert new_line >= total_lines - 2, f"expected class-end match near end (>= {total_lines-2}), got {new_line}" + assert new_line >= total_lines - \ + 2, f"expected class-end match near end (>= {total_lines-2}), got {new_line}" + def test_apply_edits_with_improved_matching(): """Test that _apply_edits_locally uses improved matching.""" - + original_code = '''using UnityEngine; public class TestClass : MonoBehaviour @@ -131,7 +143,7 @@ def test_apply_edits_with_improved_matching(): Debug.Log(message); } }''' - + # Test anchor_insert with the problematic pattern edits = [{ "op": "anchor_insert", @@ -139,30 +151,33 @@ def test_apply_edits_with_improved_matching(): "position": "before", "text": "\n public void NewMethod() { Debug.Log(\"Added at class end\"); }\n" }] - - result = manage_script_edits_module._apply_edits_locally(original_code, edits) + + result = manage_script_edits_module._apply_edits_locally( + original_code, edits) lines = result.split('\n') try: idx = next(i for i, line in enumerate(lines) if "NewMethod" in line) except StopIteration: assert False, "NewMethod not found in result" total_lines = len(lines) - assert idx >= total_lines - 5, f"method inserted too early (idx={idx}, total_lines={total_lines})" + assert idx >= total_lines - \ + 5, f"method inserted too early (idx={idx}, total_lines={total_lines})" + if __name__ == "__main__": print("Testing improved anchor matching...") print("="*60) - + success1 = test_improved_anchor_matching() - + print("\n" + "="*60) print("Comparing old vs new behavior...") success2 = test_old_vs_new_matching() - + print("\n" + "="*60) print("Testing _apply_edits_locally with improved matching...") success3 = test_apply_edits_with_improved_matching() - + print("\n" + "="*60) if success1 and success2 and success3: print("🎉 ALL TESTS PASSED! Improved anchor matching is working!") diff --git a/tests/test_logging_stdout.py b/tests/test_logging_stdout.py index 5b40fba3..9f5f8495 100644 --- a/tests/test_logging_stdout.py +++ b/tests/test_logging_stdout.py @@ -64,5 +64,7 @@ def visit_Call(self, node: ast.Call): v.visit(tree) if v.hit: offenders.append(py_file.relative_to(SRC)) - assert not syntax_errors, "syntax errors in: " + ", ".join(str(e) for e in syntax_errors) - assert not offenders, "stdout writes found in: " + ", ".join(str(o) for o in offenders) + assert not syntax_errors, "syntax errors in: " + \ + ", ".join(str(e) for e in syntax_errors) + assert not offenders, "stdout writes found in: " + \ + ", ".join(str(o) for o in offenders) diff --git a/tests/test_manage_script_uri.py b/tests/test_manage_script_uri.py index 40b64584..d2515922 100644 --- a/tests/test_manage_script_uri.py +++ b/tests/test_manage_script_uri.py @@ -1,3 +1,4 @@ +import tools.manage_script as manage_script # type: ignore import sys import types from pathlib import Path @@ -5,7 +6,6 @@ import pytest - # Locate server src dynamically to avoid hardcoded layout assumptions (same as other tests) ROOT = Path(__file__).resolve().parents[1] candidates = [ @@ -25,7 +25,12 @@ mcp_pkg = types.ModuleType("mcp") server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") -class _Dummy: pass + + +class _Dummy: + pass + + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -36,7 +41,6 @@ class _Dummy: pass # Import target module after path injection -import tools.manage_script as manage_script # type: ignore class DummyMCP: @@ -83,10 +87,13 @@ def fake_send(cmd, params): # capture params and return success @pytest.mark.parametrize( "uri, expected_name, expected_path", [ - ("file:///Users/alex/Project/Assets/Scripts/Foo%20Bar.cs", "Foo Bar", "Assets/Scripts"), + ("file:///Users/alex/Project/Assets/Scripts/Foo%20Bar.cs", + "Foo Bar", "Assets/Scripts"), ("file://localhost/Users/alex/Project/Assets/Hello.cs", "Hello", "Assets"), - ("file:///C:/Users/Alex/Proj/Assets/Scripts/Hello.cs", "Hello", "Assets/Scripts"), - ("file:///tmp/Other.cs", "Other", "tmp"), # outside Assets → fall back to normalized dir + ("file:///C:/Users/Alex/Proj/Assets/Scripts/Hello.cs", + "Hello", "Assets/Scripts"), + # outside Assets → fall back to normalized dir + ("file:///tmp/Other.cs", "Other", "tmp"), ], ) def test_split_uri_file_urls(monkeypatch, uri, expected_name, expected_path): @@ -118,9 +125,8 @@ def fake_send(cmd, params): monkeypatch.setattr(manage_script, "send_command_with_retry", fake_send) fn = tools['apply_text_edits'] - fn(DummyCtx(), uri="Assets/Scripts/Thing.cs", edits=[], precondition_sha256=None) + fn(DummyCtx(), uri="Assets/Scripts/Thing.cs", + edits=[], precondition_sha256=None) assert captured['params']['name'] == 'Thing' assert captured['params']['path'] == 'Assets/Scripts' - - diff --git a/tests/test_read_console_truncate.py b/tests/test_read_console_truncate.py index b2eafd29..6392d3bc 100644 --- a/tests/test_read_console_truncate.py +++ b/tests/test_read_console_truncate.py @@ -12,9 +12,11 @@ server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") + class _Dummy: pass + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -23,13 +25,17 @@ class _Dummy: sys.modules.setdefault("mcp.server", server_pkg) sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) + def _load_module(path: pathlib.Path, name: str): spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod -read_console_mod = _load_module(SRC / "tools" / "read_console.py", "read_console_mod") + +read_console_mod = _load_module( + SRC / "tools" / "read_console.py", "read_console_mod") + class DummyMCP: def __init__(self): @@ -41,11 +47,13 @@ def deco(fn): return fn return deco + def setup_tools(): mcp = DummyMCP() read_console_mod.register_read_console_tools(mcp) return mcp.tools + def test_read_console_full_default(monkeypatch): tools = setup_tools() read_console = tools["read_console"] @@ -60,7 +68,8 @@ def fake_send(cmd, params): } monkeypatch.setattr(read_console_mod, "send_command_with_retry", fake_send) - monkeypatch.setattr(read_console_mod, "get_unity_connection", lambda: object()) + monkeypatch.setattr( + read_console_mod, "get_unity_connection", lambda: object()) resp = read_console(ctx=None, count=10) assert resp == { @@ -85,8 +94,10 @@ def fake_send(cmd, params): } monkeypatch.setattr(read_console_mod, "send_command_with_retry", fake_send) - monkeypatch.setattr(read_console_mod, "get_unity_connection", lambda: object()) + monkeypatch.setattr( + read_console_mod, "get_unity_connection", lambda: object()) resp = read_console(ctx=None, count=10, include_stacktrace=False) - assert resp == {"success": True, "data": {"lines": [{"level": "error", "message": "oops"}]}} + assert resp == {"success": True, "data": { + "lines": [{"level": "error", "message": "oops"}]}} assert captured["params"]["includeStacktrace"] is False diff --git a/tests/test_read_resource_minimal.py b/tests/test_read_resource_minimal.py index 90d2a59b..7f68a919 100644 --- a/tests/test_read_resource_minimal.py +++ b/tests/test_read_resource_minimal.py @@ -1,3 +1,4 @@ +from tools.resource_tools import register_resource_tools # type: ignore import sys import pathlib import asyncio @@ -13,9 +14,11 @@ server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") + class _Dummy: pass + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -24,8 +27,6 @@ class _Dummy: sys.modules.setdefault("mcp.server", server_pkg) sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) -from tools.resource_tools import register_resource_tools # type: ignore - class DummyMCP: def __init__(self): @@ -57,7 +58,8 @@ def test_read_resource_minimal_metadata_only(resource_tools, tmp_path): loop = asyncio.new_event_loop() try: resp = loop.run_until_complete( - read_resource(uri="unity://path/Assets/A.txt", ctx=None, project_root=str(proj)) + read_resource(uri="unity://path/Assets/A.txt", + ctx=None, project_root=str(proj)) ) finally: loop.close() diff --git a/tests/test_resources_api.py b/tests/test_resources_api.py index 29082160..209fa4fd 100644 --- a/tests/test_resources_api.py +++ b/tests/test_resources_api.py @@ -1,3 +1,4 @@ +from tools.resource_tools import register_resource_tools # type: ignore import pytest @@ -21,17 +22,18 @@ ) sys.path.insert(0, str(SRC)) -from tools.resource_tools import register_resource_tools # type: ignore class DummyMCP: def __init__(self): self._tools = {} + def tool(self, *args, **kwargs): # accept kwargs like description def deco(fn): self._tools[fn.__name__] = fn return fn return deco + @pytest.fixture() def resource_tools(): mcp = DummyMCP() @@ -60,7 +62,8 @@ def test_resource_list_filters_and_rejects_traversal(resource_tools, tmp_path, m # Only .cs under Assets should be listed import asyncio resp = asyncio.get_event_loop().run_until_complete( - list_resources(ctx=None, pattern="*.cs", under="Assets", limit=50, project_root=str(proj)) + list_resources(ctx=None, pattern="*.cs", under="Assets", + limit=50, project_root=str(proj)) ) assert resp["success"] is True uris = resp["data"]["uris"] @@ -75,7 +78,9 @@ def test_resource_list_rejects_outside_paths(resource_tools, tmp_path): list_resources = resource_tools["list_resources"] import asyncio resp = asyncio.get_event_loop().run_until_complete( - list_resources(ctx=None, pattern="*.cs", under="..", limit=10, project_root=str(proj)) + list_resources(ctx=None, pattern="*.cs", under="..", + limit=10, project_root=str(proj)) ) assert resp["success"] is False - assert "Assets" in resp.get("error", "") or "under project root" in resp.get("error", "") + assert "Assets" in resp.get( + "error", "") or "under project root" in resp.get("error", "") diff --git a/tests/test_script_tools.py b/tests/test_script_tools.py index c7cadd35..aa14503b 100644 --- a/tests/test_script_tools.py +++ b/tests/test_script_tools.py @@ -15,9 +15,11 @@ server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") + class _Dummy: pass + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -26,14 +28,18 @@ class _Dummy: sys.modules.setdefault("mcp.server", server_pkg) sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) + def load_module(path, name): spec = importlib.util.spec_from_file_location(name, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module -manage_script_module = load_module(SRC / "tools" / "manage_script.py", "manage_script_module") -manage_asset_module = load_module(SRC / "tools" / "manage_asset.py", "manage_asset_module") + +manage_script_module = load_module( + SRC / "tools" / "manage_script.py", "manage_script_module") +manage_asset_module = load_module( + SRC / "tools" / "manage_asset.py", "manage_asset_module") class DummyMCP: @@ -46,16 +52,19 @@ def decorator(func): return func return decorator + def setup_manage_script(): mcp = DummyMCP() manage_script_module.register_manage_script_tools(mcp) return mcp.tools + def setup_manage_asset(): mcp = DummyMCP() manage_asset_module.register_manage_asset_tools(mcp) return mcp.tools + def test_apply_text_edits_long_file(monkeypatch): tools = setup_manage_script() apply_edits = tools["apply_text_edits"] @@ -66,15 +75,18 @@ def fake_send(cmd, params): captured["params"] = params return {"success": True} - monkeypatch.setattr(manage_script_module, "send_command_with_retry", fake_send) + monkeypatch.setattr(manage_script_module, + "send_command_with_retry", fake_send) - edit = {"startLine": 1005, "startCol": 0, "endLine": 1005, "endCol": 5, "newText": "Hello"} + edit = {"startLine": 1005, "startCol": 0, + "endLine": 1005, "endCol": 5, "newText": "Hello"} resp = apply_edits(None, "unity://path/Assets/Scripts/LongFile.cs", [edit]) assert captured["cmd"] == "manage_script" assert captured["params"]["action"] == "apply_text_edits" assert captured["params"]["edits"][0]["startLine"] == 1005 assert resp["success"] is True + def test_sequential_edits_use_precondition(monkeypatch): tools = setup_manage_script() apply_edits = tools["apply_text_edits"] @@ -84,12 +96,16 @@ def fake_send(cmd, params): calls.append(params) return {"success": True, "sha256": f"hash{len(calls)}"} - monkeypatch.setattr(manage_script_module, "send_command_with_retry", fake_send) + monkeypatch.setattr(manage_script_module, + "send_command_with_retry", fake_send) - edit1 = {"startLine": 1, "startCol": 0, "endLine": 1, "endCol": 0, "newText": "//header\n"} + edit1 = {"startLine": 1, "startCol": 0, "endLine": 1, + "endCol": 0, "newText": "//header\n"} resp1 = apply_edits(None, "unity://path/Assets/Scripts/File.cs", [edit1]) - edit2 = {"startLine": 2, "startCol": 0, "endLine": 2, "endCol": 0, "newText": "//second\n"} - resp2 = apply_edits(None, "unity://path/Assets/Scripts/File.cs", [edit2], precondition_sha256=resp1["sha256"]) + edit2 = {"startLine": 2, "startCol": 0, "endLine": 2, + "endCol": 0, "newText": "//second\n"} + resp2 = apply_edits(None, "unity://path/Assets/Scripts/File.cs", + [edit2], precondition_sha256=resp1["sha256"]) assert calls[1]["precondition_sha256"] == resp1["sha256"] assert resp2["sha256"] == "hash2" @@ -104,10 +120,12 @@ def fake_send(cmd, params): captured["params"] = params return {"success": True} - monkeypatch.setattr(manage_script_module, "send_command_with_retry", fake_send) + monkeypatch.setattr(manage_script_module, + "send_command_with_retry", fake_send) opts = {"validate": "relaxed", "applyMode": "atomic", "refresh": "immediate"} - apply_edits(None, "unity://path/Assets/Scripts/File.cs", [{"startLine":1,"startCol":1,"endLine":1,"endCol":1,"newText":"x"}], options=opts) + apply_edits(None, "unity://path/Assets/Scripts/File.cs", + [{"startLine": 1, "startCol": 1, "endLine": 1, "endCol": 1, "newText": "x"}], options=opts) assert captured["params"].get("options") == opts @@ -120,16 +138,20 @@ def fake_send(cmd, params): captured["params"] = params return {"success": True} - monkeypatch.setattr(manage_script_module, "send_command_with_retry", fake_send) + monkeypatch.setattr(manage_script_module, + "send_command_with_retry", fake_send) edits = [ {"startLine": 2, "startCol": 2, "endLine": 2, "endCol": 3, "newText": "A"}, - {"startLine": 3, "startCol": 2, "endLine": 3, "endCol": 2, "newText": "// tail\n"}, + {"startLine": 3, "startCol": 2, "endLine": 3, + "endCol": 2, "newText": "// tail\n"}, ] - apply_edits(None, "unity://path/Assets/Scripts/File.cs", edits, precondition_sha256="x") + apply_edits(None, "unity://path/Assets/Scripts/File.cs", + edits, precondition_sha256="x") opts = captured["params"].get("options", {}) assert opts.get("applyMode") == "atomic" + def test_manage_asset_prefab_modify_request(monkeypatch): tools = setup_manage_asset() manage_asset = tools["manage_asset"] @@ -140,8 +162,10 @@ async def fake_async(cmd, params, loop=None): captured["params"] = params return {"success": True} - monkeypatch.setattr(manage_asset_module, "async_send_command_with_retry", fake_async) - monkeypatch.setattr(manage_asset_module, "get_unity_connection", lambda: object()) + monkeypatch.setattr(manage_asset_module, + "async_send_command_with_retry", fake_async) + monkeypatch.setattr(manage_asset_module, + "get_unity_connection", lambda: object()) async def run(): resp = await manage_asset( diff --git a/tests/test_telemetry_endpoint_validation.py b/tests/test_telemetry_endpoint_validation.py index 8ba0d27b..16de719c 100644 --- a/tests/test_telemetry_endpoint_validation.py +++ b/tests/test_telemetry_endpoint_validation.py @@ -1,18 +1,21 @@ import os import importlib + def test_endpoint_rejects_non_http(tmp_path, monkeypatch): # Point data dir to temp to avoid touching real files monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "file:///etc/passwd") - telemetry = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.telemetry") + telemetry = importlib.import_module( + "UnityMcpBridge.UnityMcpServer~.src.telemetry") importlib.reload(telemetry) tc = telemetry.TelemetryCollector() # Should have fallen back to default endpoint assert tc.config.endpoint == tc.config.default_endpoint + def test_config_preferred_then_env_override(tmp_path, monkeypatch): # Simulate config telemetry endpoint monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) @@ -20,27 +23,32 @@ def test_config_preferred_then_env_override(tmp_path, monkeypatch): # Patch config.telemetry_endpoint via import mocking import importlib - cfg_mod = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.config") + cfg_mod = importlib.import_module( + "UnityMcpBridge.UnityMcpServer~.src.config") old_endpoint = cfg_mod.config.telemetry_endpoint cfg_mod.config.telemetry_endpoint = "https://example.com/telemetry" try: - telemetry = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.telemetry") + telemetry = importlib.import_module( + "UnityMcpBridge.UnityMcpServer~.src.telemetry") importlib.reload(telemetry) tc = telemetry.TelemetryCollector() assert tc.config.endpoint == "https://example.com/telemetry" # Env should override config - monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "https://override.example/ep") + monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", + "https://override.example/ep") importlib.reload(telemetry) tc2 = telemetry.TelemetryCollector() assert tc2.config.endpoint == "https://override.example/ep" finally: cfg_mod.config.telemetry_endpoint = old_endpoint + def test_uuid_preserved_on_malformed_milestones(tmp_path, monkeypatch): monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) - telemetry = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.telemetry") + telemetry = importlib.import_module( + "UnityMcpBridge.UnityMcpServer~.src.telemetry") importlib.reload(telemetry) tc1 = telemetry.TelemetryCollector() @@ -53,4 +61,3 @@ def test_uuid_preserved_on_malformed_milestones(tmp_path, monkeypatch): importlib.reload(telemetry) tc2 = telemetry.TelemetryCollector() assert tc2._customer_uuid == first_uuid - diff --git a/tests/test_telemetry_queue_worker.py b/tests/test_telemetry_queue_worker.py index 09e4f90f..d323d094 100644 --- a/tests/test_telemetry_queue_worker.py +++ b/tests/test_telemetry_queue_worker.py @@ -16,9 +16,11 @@ server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") + class _Dummy: pass + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -72,12 +74,12 @@ def slow_send(self, rec): time.sleep(0.3) # Verify drops were logged (queue full backpressure) - dropped_logs = [m for m in caplog.messages if "Telemetry queue full; dropping" in m] + dropped_logs = [ + m for m in caplog.messages if "Telemetry queue full; dropping" in m] assert len(dropped_logs) >= 1 # Ensure only one worker thread exists and is alive assert collector._worker.is_alive() - worker_threads = [t for t in threading.enumerate() if t is collector._worker] + worker_threads = [ + t for t in threading.enumerate() if t is collector._worker] assert len(worker_threads) == 1 - - diff --git a/tests/test_telemetry_subaction.py b/tests/test_telemetry_subaction.py index c1c597e2..b74b5ea1 100644 --- a/tests/test_telemetry_subaction.py +++ b/tests/test_telemetry_subaction.py @@ -3,7 +3,8 @@ def _get_decorator_module(): # Import the telemetry_decorator module from the Unity MCP server src - mod = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.telemetry_decorator") + mod = importlib.import_module( + "UnityMcpBridge.UnityMcpServer~.src.telemetry_decorator") return mod @@ -79,5 +80,3 @@ def dummy_tool_without_action(ctx, name: str): _ = wrapped(None, name="X") assert captured["tool_name"] == "apply_text_edits" assert captured["sub_action"] is None - - diff --git a/tests/test_transport_framing.py b/tests/test_transport_framing.py index 42f93701..882f912c 100644 --- a/tests/test_transport_framing.py +++ b/tests/test_transport_framing.py @@ -1,3 +1,4 @@ +from unity_connection import UnityConnection import sys import json import struct @@ -24,8 +25,6 @@ ) sys.path.insert(0, str(SRC)) -from unity_connection import UnityConnection - def start_dummy_server(greeting: bytes, respond_ping: bool = False): """Start a minimal TCP server for handshake tests.""" @@ -159,7 +158,10 @@ def test_unframed_data_disconnect(): def test_zero_length_payload_heartbeat(): # Server that sends handshake and a zero-length heartbeat frame followed by a pong payload - import socket, struct, threading, time + import socket + import struct + import threading + import time sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("127.0.0.1", 0)) @@ -181,8 +183,10 @@ def _run(): conn.sendall(struct.pack(">Q", len(payload)) + payload) time.sleep(0.02) finally: - try: conn.close() - except Exception: pass + try: + conn.close() + except Exception: + pass sock.close() threading.Thread(target=_run, daemon=True).start() diff --git a/tests/test_validate_script_summary.py b/tests/test_validate_script_summary.py index 86a8c057..f9638128 100644 --- a/tests/test_validate_script_summary.py +++ b/tests/test_validate_script_summary.py @@ -12,9 +12,11 @@ server_pkg = types.ModuleType("mcp.server") fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") + class _Dummy: pass + fastmcp_pkg.FastMCP = _Dummy fastmcp_pkg.Context = _Dummy server_pkg.fastmcp = fastmcp_pkg @@ -23,13 +25,17 @@ class _Dummy: sys.modules.setdefault("mcp.server", server_pkg) sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) + def _load_module(path: pathlib.Path, name: str): spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod -manage_script = _load_module(SRC / "tools" / "manage_script.py", "manage_script_mod") + +manage_script = _load_module( + SRC / "tools" / "manage_script.py", "manage_script_mod") + class DummyMCP: def __init__(self): @@ -41,11 +47,13 @@ def deco(fn): return fn return deco + def setup_tools(): mcp = DummyMCP() manage_script.register_manage_script_tools(mcp) return mcp.tools + def test_validate_script_returns_counts(monkeypatch): tools = setup_tools() validate_script = tools["validate_script"] diff --git a/tools/stress_mcp.py b/tools/stress_mcp.py index bd14c35a..47b1675c 100644 --- a/tools/stress_mcp.py +++ b/tools/stress_mcp.py @@ -21,7 +21,8 @@ def dlog(*args): def find_status_files() -> list[Path]: home = Path.home() - status_dir = Path(os.environ.get("UNITY_MCP_STATUS_DIR", home / ".unity-mcp")) + status_dir = Path(os.environ.get( + "UNITY_MCP_STATUS_DIR", home / ".unity-mcp")) if not status_dir.exists(): return [] return sorted(status_dir.glob("unity-mcp-status-*.json"), key=lambda p: p.stat().st_mtime, reverse=True) @@ -87,7 +88,8 @@ def make_ping_frame() -> bytes: def make_execute_menu_item(menu_path: str) -> bytes: # Retained for manual debugging; not used in normal stress runs - payload = {"type": "execute_menu_item", "params": {"action": "execute", "menu_path": menu_path}} + payload = {"type": "execute_menu_item", "params": { + "action": "execute", "menu_path": menu_path}} return json.dumps(payload).encode("utf-8") @@ -102,7 +104,8 @@ async def client_loop(idx: int, host: str, port: int, stop_time: float, stats: d await asyncio.wait_for(do_handshake(reader), timeout=TIMEOUT) # Send a quick ping first await write_frame(writer, make_ping_frame()) - _ = await asyncio.wait_for(read_frame(reader), timeout=TIMEOUT) # ignore content + # ignore content + _ = await asyncio.wait_for(read_frame(reader), timeout=TIMEOUT) # Main activity loop (keep-alive + light load). Edit spam handled by reload_churn_task. while time.time() < stop_time: @@ -182,7 +185,8 @@ async def reload_churn_task(project_path: str, stop_time: float, unity_file: str if relative: # Derive name and directory for ManageScript and compute precondition SHA + EOF position name_base = Path(relative).stem - dir_path = str(Path(relative).parent).replace('\\', '/') + dir_path = str( + Path(relative).parent).replace('\\', '/') # 1) Read current contents via manage_script.read to compute SHA and true EOF location contents = None @@ -203,8 +207,10 @@ async def reload_churn_task(project_path: str, stop_time: float, unity_file: str await write_frame(writer, json.dumps(read_payload).encode("utf-8")) resp = await asyncio.wait_for(read_frame(reader), timeout=TIMEOUT) - read_obj = json.loads(resp.decode("utf-8", errors="ignore")) - result = read_obj.get("result", read_obj) if isinstance(read_obj, dict) else {} + read_obj = json.loads( + resp.decode("utf-8", errors="ignore")) + result = read_obj.get("result", read_obj) if isinstance( + read_obj, dict) else {} if result.get("success"): data_obj = result.get("data", {}) contents = data_obj.get("contents") or "" @@ -222,13 +228,15 @@ async def reload_churn_task(project_path: str, stop_time: float, unity_file: str pass if not read_success or contents is None: - stats["apply_errors"] = stats.get("apply_errors", 0) + 1 + stats["apply_errors"] = stats.get( + "apply_errors", 0) + 1 await asyncio.sleep(0.5) continue # Compute SHA and EOF insertion point import hashlib - sha = hashlib.sha256(contents.encode("utf-8")).hexdigest() + sha = hashlib.sha256( + contents.encode("utf-8")).hexdigest() lines = contents.splitlines(keepends=True) # Insert at true EOF (safe against header guards) end_line = len(lines) + 1 # 1-based exclusive end @@ -237,7 +245,8 @@ async def reload_churn_task(project_path: str, stop_time: float, unity_file: str # Build a unique marker append; ensure it begins with a newline if needed marker = f"// MCP_STRESS seq={seq} time={int(time.time())}" seq += 1 - insert_text = ("\n" if not contents.endswith("\n") else "") + marker + "\n" + insert_text = ("\n" if not contents.endswith( + "\n") else "") + marker + "\n" # 2) Apply text edits with immediate refresh and precondition apply_payload = { @@ -269,11 +278,14 @@ async def reload_churn_task(project_path: str, stop_time: float, unity_file: str await write_frame(writer, json.dumps(apply_payload).encode("utf-8")) resp = await asyncio.wait_for(read_frame(reader), timeout=TIMEOUT) try: - data = json.loads(resp.decode("utf-8", errors="ignore")) - result = data.get("result", data) if isinstance(data, dict) else {} + data = json.loads(resp.decode( + "utf-8", errors="ignore")) + result = data.get("result", data) if isinstance( + data, dict) else {} ok = bool(result.get("success", False)) if ok: - stats["applies"] = stats.get("applies", 0) + 1 + stats["applies"] = stats.get( + "applies", 0) + 1 apply_success = True break except Exception: @@ -290,7 +302,8 @@ async def reload_churn_task(project_path: str, stop_time: float, unity_file: str except Exception: pass if not apply_success: - stats["apply_errors"] = stats.get("apply_errors", 0) + 1 + stats["apply_errors"] = stats.get( + "apply_errors", 0) + 1 except Exception: pass @@ -298,13 +311,17 @@ async def reload_churn_task(project_path: str, stop_time: float, unity_file: str async def main(): - ap = argparse.ArgumentParser(description="Stress test the Unity MCP bridge with concurrent clients and reload churn") + ap = argparse.ArgumentParser( + description="Stress test the Unity MCP bridge with concurrent clients and reload churn") ap.add_argument("--host", default="127.0.0.1") - ap.add_argument("--project", default=str(Path(__file__).resolve().parents[1] / "TestProjects" / "UnityMCPTests")) - ap.add_argument("--unity-file", default=str(Path(__file__).resolve().parents[1] / "TestProjects" / "UnityMCPTests" / "Assets" / "Scripts" / "LongUnityScriptClaudeTest.cs")) + ap.add_argument("--project", default=str( + Path(__file__).resolve().parents[1] / "TestProjects" / "UnityMCPTests")) + ap.add_argument("--unity-file", default=str(Path(__file__).resolve( + ).parents[1] / "TestProjects" / "UnityMCPTests" / "Assets" / "Scripts" / "LongUnityScriptClaudeTest.cs")) ap.add_argument("--clients", type=int, default=10) ap.add_argument("--duration", type=int, default=60) - ap.add_argument("--storm-count", type=int, default=1, help="Number of scripts to touch each cycle") + ap.add_argument("--storm-count", type=int, default=1, + help="Number of scripts to touch each cycle") args = ap.parse_args() port = discover_port(args.project) @@ -315,10 +332,12 @@ async def main(): # Spawn clients for i in range(max(1, args.clients)): - tasks.append(asyncio.create_task(client_loop(i, args.host, port, stop_time, stats))) + tasks.append(asyncio.create_task( + client_loop(i, args.host, port, stop_time, stats))) # Spawn reload churn task - tasks.append(asyncio.create_task(reload_churn_task(args.project, stop_time, args.unity_file, args.host, port, stats, storm_count=args.storm_count))) + tasks.append(asyncio.create_task(reload_churn_task(args.project, stop_time, + args.unity_file, args.host, port, stats, storm_count=args.storm_count))) await asyncio.gather(*tasks, return_exceptions=True) print(json.dumps({"port": port, "stats": stats}, indent=2)) @@ -329,5 +348,3 @@ async def main(): asyncio.run(main()) except KeyboardInterrupt: pass - - From 7c23f245aba3261224129f1106c276f98e021373 Mon Sep 17 00:00:00 2001 From: Justin Barnett Date: Fri, 3 Oct 2025 16:43:40 -0400 Subject: [PATCH 2/7] feat: Unity Asset Store compliance with post-installation dependency setup (#281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement Unity Asset Store compliance with post-installation dependency setup - Remove bundled Python dependencies from Unity package - Add comprehensive 6-step setup wizard with auto-trigger on first import - Implement cross-platform dependency detection (Windows, macOS, Linux) - Add integrated MCP client configuration within setup process - Create production-ready menu structure with clean UI/UX - Ensure complete end-to-end setup requiring no additional configuration - Add comprehensive error handling and recovery mechanisms This implementation ensures Asset Store compliance while maintaining full functionality through guided user setup. Users are left 100% ready to use MCP after completing the setup wizard. * refactor: improve Asset Store compliance implementation with production-ready setup - Remove automatic installation attempts on package import - Always show setup wizard on package install/reinstall - Integrate MCP client configuration as part of setup wizard process - Ensure MCP client config window remains accessible via menu - Remove testing components for production readiness - Replace automatic installation with manual guidance only - Add complete 4-step setup flow: Welcome → Dependencies → Installation Guide → Client Configuration → Complete - Improve user experience with clear instructions and accessible client management * feat: add comprehensive dependency requirement warnings - Add critical warnings throughout setup wizard that package cannot function without dependencies - Update package.json description to clearly state manual dependency installation requirement - Prevent setup completion if dependencies are missing - Enhance skip setup warning to emphasize package will be non-functional - Add error messages explaining consequences of missing dependencies - Update menu item to indicate setup wizard is required - Ensure users understand package is completely non-functional without proper dependency installation * refactor: simplify setup wizard for production BREAKING: Reduced setup wizard from 5 steps to 3 streamlined steps: - Step 1: Setup (welcome + dependency check + installation guide) - Step 2: Configure (client configuration with direct access to full settings) - Step 3: Complete (final status and quick access to resources) Simplifications: - Consolidated UI components with DRY helper methods (DrawSectionTitle, DrawSuccessStatus, DrawErrorStatus) - Simplified dependency status display with clean icons and essential info - Removed complex state management - using simple EditorPrefs instead - Removed unused InstallationOrchestrator and SetupState classes - Streamlined client configuration to direct users to full settings window - Simplified navigation with back/skip/next buttons - Reduced code complexity while maintaining solid principles Results: - 40% less code while maintaining all functionality - Cleaner, more intuitive user flow - Faster setup process with fewer clicks - Production-ready simplicity - Easier maintenance and debugging * fix: add missing using statement for DependencyCheckResult Add missing 'using MCPForUnity.Editor.Dependencies.Models;' to resolve DependencyCheckResult type reference in SetupWizard.cs * refactor: optimize dependency checks and remove dead code * fix: remove unused DrawInstallationProgressStep method Removes leftover method that references deleted _isInstalling and _installationStatus fields, fixing compilation errors. * feat: improve setup wizard UX and add real client configuration 1. Remove dependency mentions from package.json description 2. Only show dependency warnings when dependencies are actually missing 3. Add actual MCP client configuration functionality within the wizard: - Client selection dropdown - Individual client configuration - Claude Code registration/unregistration - Batch configuration for all clients - Manual setup instructions - Real configuration file writing Users can now complete full setup including client configuration without leaving the wizard. * refactor: improve menu text and client restart tip - Remove '(Required)' from Setup Wizard menu item for cleaner appearance - Update tip to reflect that most AI clients auto-detect configuration changes * refactor: simplify client restart tip message * fix: add missing using statement for MCPForUnityEditorWindow Add 'using MCPForUnity.Editor.Windows;' to resolve unresolved symbol error for MCPForUnityEditorWindow in SetupWizard.cs * Format code * Remove unused folders * Same for validators * Same for Setup... * feat: add setup wizard persistence to avoid showing on subsequent imports * fix: update Python version check to support Python 4+ across all platform detectors * refactor: extract common platform detection logic into PlatformDetectorBase class * feat: add configuration helpers for MCP client setup with sophisticated path resolution * fix: add missing override keyword to DetectPython method in platform detectors * fix: update menu item labels for consistent capitalization and naming * fix: standardize "MCP For Unity" capitalization in window titles and dialogs * refactor: update package ID from justinpbarnett to coplaydev across codebase * refactor: remove unused validation and configuration helper methods * refactor: remove unused warnOnLegacyPackageId parameter from TryFindEmbeddedServerSource --------- Co-authored-by: Claude Co-authored-by: Marcus Sanatan --- UnityMcpBridge/Editor/Dependencies.meta | 8 + .../Editor/Dependencies/DependencyManager.cs | 308 ++++++++ .../Dependencies/DependencyManager.cs.meta | 11 + .../Editor/Dependencies/Models.meta | 8 + .../Models/DependencyCheckResult.cs | 96 +++ .../Models/DependencyCheckResult.cs.meta | 11 + .../Dependencies/Models/DependencyStatus.cs | 65 ++ .../Models/DependencyStatus.cs.meta | 11 + .../Dependencies/PlatformDetectors.meta | 8 + .../PlatformDetectors/IPlatformDetector.cs | 50 ++ .../IPlatformDetector.cs.meta | 11 + .../LinuxPlatformDetector.cs | 253 ++++++ .../LinuxPlatformDetector.cs.meta | 11 + .../MacOSPlatformDetector.cs | 253 ++++++ .../MacOSPlatformDetector.cs.meta | 11 + .../PlatformDetectors/PlatformDetectorBase.cs | 161 ++++ .../PlatformDetectorBase.cs.meta | 11 + .../WindowsPlatformDetector.cs | 232 ++++++ .../WindowsPlatformDetector.cs.meta | 11 + .../Editor/Helpers/McpConfigurationHelper.cs | 297 +++++++ .../Helpers/McpConfigurationHelper.cs.meta | 11 + .../Editor/Helpers/McpPathResolver.cs | 123 +++ .../Editor/Helpers/McpPathResolver.cs.meta | 11 + .../Editor/Helpers/PackageInstaller.cs | 2 +- .../Editor/Helpers/ServerPathResolver.cs | 20 +- UnityMcpBridge/Editor/Setup.meta | 8 + UnityMcpBridge/Editor/Setup/SetupWizard.cs | 150 ++++ .../Editor/Setup/SetupWizard.cs.meta | 11 + .../Editor/Setup/SetupWizardWindow.cs | 726 ++++++++++++++++++ .../Editor/Setup/SetupWizardWindow.cs.meta | 11 + .../Editor/Windows/MCPForUnityEditorWindow.cs | 439 +---------- UnityMcpBridge/package.json | 2 +- 32 files changed, 2903 insertions(+), 438 deletions(-) create mode 100644 UnityMcpBridge/Editor/Dependencies.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/DependencyManager.cs create mode 100644 UnityMcpBridge/Editor/Dependencies/DependencyManager.cs.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/Models.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs create mode 100644 UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs create mode 100644 UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs create mode 100644 UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta create mode 100644 UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs create mode 100644 UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs.meta create mode 100644 UnityMcpBridge/Editor/Helpers/McpPathResolver.cs create mode 100644 UnityMcpBridge/Editor/Helpers/McpPathResolver.cs.meta create mode 100644 UnityMcpBridge/Editor/Setup.meta create mode 100644 UnityMcpBridge/Editor/Setup/SetupWizard.cs create mode 100644 UnityMcpBridge/Editor/Setup/SetupWizard.cs.meta create mode 100644 UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs create mode 100644 UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs.meta diff --git a/UnityMcpBridge/Editor/Dependencies.meta b/UnityMcpBridge/Editor/Dependencies.meta new file mode 100644 index 00000000..77685d17 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 221a4d6e595be6897a5b17b77aedd4d0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs new file mode 100644 index 00000000..2f7b5ca1 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Dependencies.PlatformDetectors; +using MCPForUnity.Editor.Helpers; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Dependencies +{ + /// + /// Main orchestrator for dependency validation and management + /// + public static class DependencyManager + { + private static readonly List _detectors = new List + { + new WindowsPlatformDetector(), + new MacOSPlatformDetector(), + new LinuxPlatformDetector() + }; + + private static IPlatformDetector _currentDetector; + + /// + /// Get the platform detector for the current operating system + /// + public static IPlatformDetector GetCurrentPlatformDetector() + { + if (_currentDetector == null) + { + _currentDetector = _detectors.FirstOrDefault(d => d.CanDetect); + if (_currentDetector == null) + { + throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}"); + } + } + return _currentDetector; + } + + /// + /// Perform a comprehensive dependency check + /// + public static DependencyCheckResult CheckAllDependencies() + { + var result = new DependencyCheckResult(); + + try + { + var detector = GetCurrentPlatformDetector(); + McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false); + + // Check Python + var pythonStatus = detector.DetectPython(); + result.Dependencies.Add(pythonStatus); + + // Check UV + var uvStatus = detector.DetectUV(); + result.Dependencies.Add(uvStatus); + + // Check MCP Server + var serverStatus = detector.DetectMCPServer(); + result.Dependencies.Add(serverStatus); + + // Generate summary and recommendations + result.GenerateSummary(); + GenerateRecommendations(result, detector); + + McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false); + } + catch (Exception ex) + { + McpLog.Error($"Error during dependency check: {ex.Message}"); + result.Summary = $"Dependency check failed: {ex.Message}"; + result.IsSystemReady = false; + } + + return result; + } + + /// + /// Quick check if system is ready for MCP operations + /// + public static bool IsSystemReady() + { + try + { + var result = CheckAllDependencies(); + return result.IsSystemReady; + } + catch + { + return false; + } + } + + /// + /// Get a summary of missing dependencies + /// + public static string GetMissingDependenciesSummary() + { + try + { + var result = CheckAllDependencies(); + var missing = result.GetMissingRequired(); + + if (missing.Count == 0) + { + return "All required dependencies are available."; + } + + var names = missing.Select(d => d.Name).ToArray(); + return $"Missing required dependencies: {string.Join(", ", names)}"; + } + catch (Exception ex) + { + return $"Error checking dependencies: {ex.Message}"; + } + } + + /// + /// Check if a specific dependency is available + /// + public static bool IsDependencyAvailable(string dependencyName) + { + try + { + var detector = GetCurrentPlatformDetector(); + + return dependencyName.ToLowerInvariant() switch + { + "python" => detector.DetectPython().IsAvailable, + "uv" => detector.DetectUV().IsAvailable, + "mcpserver" or "mcp-server" => detector.DetectMCPServer().IsAvailable, + _ => false + }; + } + catch + { + return false; + } + } + + /// + /// Get installation recommendations for the current platform + /// + public static string GetInstallationRecommendations() + { + try + { + var detector = GetCurrentPlatformDetector(); + return detector.GetInstallationRecommendations(); + } + catch (Exception ex) + { + return $"Error getting installation recommendations: {ex.Message}"; + } + } + + /// + /// Get platform-specific installation URLs + /// + public static (string pythonUrl, string uvUrl) GetInstallationUrls() + { + try + { + var detector = GetCurrentPlatformDetector(); + return (detector.GetPythonInstallUrl(), detector.GetUVInstallUrl()); + } + catch + { + return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/"); + } + } + + /// + /// Validate that the MCP server can be started + /// + public static bool ValidateMCPServerStartup() + { + try + { + // Check if Python and UV are available + if (!IsDependencyAvailable("python") || !IsDependencyAvailable("uv")) + { + return false; + } + + // Try to ensure server is installed + ServerInstaller.EnsureServerInstalled(); + + // Check if server files exist + var serverStatus = GetCurrentPlatformDetector().DetectMCPServer(); + return serverStatus.IsAvailable; + } + catch (Exception ex) + { + McpLog.Error($"Error validating MCP server startup: {ex.Message}"); + return false; + } + } + + /// + /// Attempt to repair the Python environment + /// + public static bool RepairPythonEnvironment() + { + try + { + McpLog.Info("Attempting to repair Python environment..."); + return ServerInstaller.RepairPythonEnvironment(); + } + catch (Exception ex) + { + McpLog.Error($"Error repairing Python environment: {ex.Message}"); + return false; + } + } + + /// + /// Get detailed dependency information for diagnostics + /// + public static string GetDependencyDiagnostics() + { + try + { + var result = CheckAllDependencies(); + var detector = GetCurrentPlatformDetector(); + + var diagnostics = new System.Text.StringBuilder(); + diagnostics.AppendLine($"Platform: {detector.PlatformName}"); + diagnostics.AppendLine($"Check Time: {result.CheckedAt:yyyy-MM-dd HH:mm:ss} UTC"); + diagnostics.AppendLine($"System Ready: {result.IsSystemReady}"); + diagnostics.AppendLine(); + + foreach (var dep in result.Dependencies) + { + diagnostics.AppendLine($"=== {dep.Name} ==="); + diagnostics.AppendLine($"Available: {dep.IsAvailable}"); + diagnostics.AppendLine($"Required: {dep.IsRequired}"); + + if (!string.IsNullOrEmpty(dep.Version)) + diagnostics.AppendLine($"Version: {dep.Version}"); + + if (!string.IsNullOrEmpty(dep.Path)) + diagnostics.AppendLine($"Path: {dep.Path}"); + + if (!string.IsNullOrEmpty(dep.Details)) + diagnostics.AppendLine($"Details: {dep.Details}"); + + if (!string.IsNullOrEmpty(dep.ErrorMessage)) + diagnostics.AppendLine($"Error: {dep.ErrorMessage}"); + + diagnostics.AppendLine(); + } + + if (result.RecommendedActions.Count > 0) + { + diagnostics.AppendLine("=== Recommended Actions ==="); + foreach (var action in result.RecommendedActions) + { + diagnostics.AppendLine($"- {action}"); + } + } + + return diagnostics.ToString(); + } + catch (Exception ex) + { + return $"Error generating diagnostics: {ex.Message}"; + } + } + + private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector) + { + var missing = result.GetMissingDependencies(); + + if (missing.Count == 0) + { + result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity."); + return; + } + + foreach (var dep in missing) + { + if (dep.Name == "Python") + { + result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}"); + } + else if (dep.Name == "UV Package Manager") + { + result.RecommendedActions.Add($"Install UV package manager from: {detector.GetUVInstallUrl()}"); + } + else if (dep.Name == "MCP Server") + { + result.RecommendedActions.Add("MCP Server will be installed automatically when needed."); + } + } + + if (result.GetMissingRequired().Count > 0) + { + result.RecommendedActions.Add("Use the Setup Wizard (Window > MCP for Unity > Setup Wizard) for guided installation."); + } + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs.meta b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs.meta new file mode 100644 index 00000000..ae03260a --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6789012345678901234abcdef012345 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/Models.meta b/UnityMcpBridge/Editor/Dependencies/Models.meta new file mode 100644 index 00000000..2174dd52 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b2c3d4e5f6789012345678901234abcd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs b/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs new file mode 100644 index 00000000..5dd2edaf --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MCPForUnity.Editor.Dependencies.Models +{ + /// + /// Result of a comprehensive dependency check + /// + [Serializable] + public class DependencyCheckResult + { + /// + /// List of all dependency statuses checked + /// + public List Dependencies { get; set; } + + /// + /// Overall system readiness for MCP operations + /// + public bool IsSystemReady { get; set; } + + /// + /// Whether all required dependencies are available + /// + public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false; + + /// + /// Whether any optional dependencies are missing + /// + public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false; + + /// + /// Summary message about the dependency state + /// + public string Summary { get; set; } + + /// + /// Recommended next steps for the user + /// + public List RecommendedActions { get; set; } + + /// + /// Timestamp when this check was performed + /// + public DateTime CheckedAt { get; set; } + + public DependencyCheckResult() + { + Dependencies = new List(); + RecommendedActions = new List(); + CheckedAt = DateTime.UtcNow; + } + + /// + /// Get dependencies by availability status + /// + public List GetMissingDependencies() + { + return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List(); + } + + /// + /// Get missing required dependencies + /// + public List GetMissingRequired() + { + return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List(); + } + + /// + /// Generate a user-friendly summary of the dependency state + /// + public void GenerateSummary() + { + var missing = GetMissingDependencies(); + var missingRequired = GetMissingRequired(); + + if (missing.Count == 0) + { + Summary = "All dependencies are available and ready."; + IsSystemReady = true; + } + else if (missingRequired.Count == 0) + { + Summary = $"System is ready. {missing.Count} optional dependencies are missing."; + IsSystemReady = true; + } + else + { + Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing."; + IsSystemReady = false; + } + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs.meta b/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs.meta new file mode 100644 index 00000000..a88c3bb2 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 789012345678901234abcdef01234567 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs b/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs new file mode 100644 index 00000000..e755ecad --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs @@ -0,0 +1,65 @@ +using System; + +namespace MCPForUnity.Editor.Dependencies.Models +{ + /// + /// Represents the status of a dependency check + /// + [Serializable] + public class DependencyStatus + { + /// + /// Name of the dependency being checked + /// + public string Name { get; set; } + + /// + /// Whether the dependency is available and functional + /// + public bool IsAvailable { get; set; } + + /// + /// Version information if available + /// + public string Version { get; set; } + + /// + /// Path to the dependency executable/installation + /// + public string Path { get; set; } + + /// + /// Additional details about the dependency status + /// + public string Details { get; set; } + + /// + /// Error message if dependency check failed + /// + public string ErrorMessage { get; set; } + + /// + /// Whether this dependency is required for basic functionality + /// + public bool IsRequired { get; set; } + + /// + /// Suggested installation method or URL + /// + public string InstallationHint { get; set; } + + public DependencyStatus(string name, bool isRequired = true) + { + Name = name; + IsRequired = isRequired; + IsAvailable = false; + } + + public override string ToString() + { + var status = IsAvailable ? "✓" : "✗"; + var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : ""; + return $"{status} {Name}{version}"; + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs.meta b/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs.meta new file mode 100644 index 00000000..d6eb1d59 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6789012345678901234abcdef0123456 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors.meta new file mode 100644 index 00000000..22a6b1db --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c3d4e5f6789012345678901234abcdef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs new file mode 100644 index 00000000..7fba58f9 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs @@ -0,0 +1,50 @@ +using MCPForUnity.Editor.Dependencies.Models; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Interface for platform-specific dependency detection + /// + public interface IPlatformDetector + { + /// + /// Platform name this detector handles + /// + string PlatformName { get; } + + /// + /// Whether this detector can run on the current platform + /// + bool CanDetect { get; } + + /// + /// Detect Python installation on this platform + /// + DependencyStatus DetectPython(); + + /// + /// Detect UV package manager on this platform + /// + DependencyStatus DetectUV(); + + /// + /// Detect MCP server installation on this platform + /// + DependencyStatus DetectMCPServer(); + + /// + /// Get platform-specific installation recommendations + /// + string GetInstallationRecommendations(); + + /// + /// Get platform-specific Python installation URL + /// + string GetPythonInstallUrl(); + + /// + /// Get platform-specific UV installation URL + /// + string GetUVInstallUrl(); + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta new file mode 100644 index 00000000..d2cd9f07 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9012345678901234abcdef0123456789 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs new file mode 100644 index 00000000..09fded14 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs @@ -0,0 +1,253 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Linux-specific dependency detection + /// + public class LinuxPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "Linux"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths on Linux + var candidates = new[] + { + "python3", + "python", + "/usr/bin/python3", + "/usr/local/bin/python3", + "/opt/python/bin/python3", + "/snap/bin/python3" + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'which' command + if (TryFindInPath("python3", out string pathResult) || + TryFindInPath("python", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths including system, snap, and user-local locations."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://www.python.org/downloads/source/"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#linux"; + } + + public override string GetInstallationRecommendations() + { + return @"Linux Installation Recommendations: + +1. Python: Install via package manager or pyenv + - Ubuntu/Debian: sudo apt install python3 python3-pip + - Fedora/RHEL: sudo dnf install python3 python3-pip + - Arch: sudo pacman -S python python-pip + - Or use pyenv: https://github.com/pyenv/pyenv + +2. UV Package Manager: Install via curl + - Run: curl -LsSf https://astral.sh/uv/install.sh | sh + - Or download from: https://github.com/astral-sh/uv/releases + +3. MCP Server: Will be installed automatically by Unity MCP Bridge + +Note: Make sure ~/.local/bin is in your PATH for user-local installations."; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Set PATH to include common locations + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/usr/local/bin", + "/usr/bin", + "/bin", + "/snap/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Enhance PATH for Unity's GUI environment + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/usr/local/bin", + "/usr/bin", + "/bin", + "/snap/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + fullPath = output; + return true; + } + } + catch + { + // Ignore errors + } + + return false; + } + + private bool TryParseVersion(string version, out int major, out int minor) + { + return base.TryParseVersion(version, out major, out minor); + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta new file mode 100644 index 00000000..4f8267fd --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2345678901234abcdef0123456789abc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs new file mode 100644 index 00000000..715338ce --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs @@ -0,0 +1,253 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// macOS-specific dependency detection + /// + public class MacOSPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "macOS"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths on macOS + var candidates = new[] + { + "python3", + "python", + "/usr/bin/python3", + "/usr/local/bin/python3", + "/opt/homebrew/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.10/bin/python3" + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'which' command + if (TryFindInPath("python3", out string pathResult) || + TryFindInPath("python", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths including Homebrew, Framework, and system locations."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://www.python.org/downloads/macos/"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#macos"; + } + + public override string GetInstallationRecommendations() + { + return @"macOS Installation Recommendations: + +1. Python: Install via Homebrew (recommended) or python.org + - Homebrew: brew install python3 + - Direct download: https://python.org/downloads/macos/ + +2. UV Package Manager: Install via curl or Homebrew + - Curl: curl -LsSf https://astral.sh/uv/install.sh | sh + - Homebrew: brew install uv + +3. MCP Server: Will be installed automatically by Unity MCP Bridge + +Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH."; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Set PATH to include common locations + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Enhance PATH for Unity's GUI environment + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + fullPath = output; + return true; + } + } + catch + { + // Ignore errors + } + + return false; + } + + private bool TryParseVersion(string version, out int major, out int minor) + { + return base.TryParseVersion(version, out major, out minor); + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta new file mode 100644 index 00000000..b43864a2 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12345678901234abcdef0123456789ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs new file mode 100644 index 00000000..98044f17 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs @@ -0,0 +1,161 @@ +using System; +using System.Diagnostics; +using System.IO; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Base class for platform-specific dependency detection + /// + public abstract class PlatformDetectorBase : IPlatformDetector + { + public abstract string PlatformName { get; } + public abstract bool CanDetect { get; } + + public abstract DependencyStatus DetectPython(); + public abstract string GetPythonInstallUrl(); + public abstract string GetUVInstallUrl(); + public abstract string GetInstallationRecommendations(); + + public virtual DependencyStatus DetectUV() + { + var status = new DependencyStatus("UV Package Manager", isRequired: true) + { + InstallationHint = GetUVInstallUrl() + }; + + try + { + // Use existing UV detection from ServerInstaller + string uvPath = ServerInstaller.FindUvPath(); + if (!string.IsNullOrEmpty(uvPath)) + { + if (TryValidateUV(uvPath, out string version)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = uvPath; + status.Details = $"Found UV {version} at {uvPath}"; + return status; + } + } + + status.ErrorMessage = "UV package manager not found. Please install UV."; + status.Details = "UV is required for managing Python dependencies."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting UV: {ex.Message}"; + } + + return status; + } + + public virtual DependencyStatus DetectMCPServer() + { + var status = new DependencyStatus("MCP Server", isRequired: false); + + try + { + // Check if server is installed + string serverPath = ServerInstaller.GetServerPath(); + string serverPy = Path.Combine(serverPath, "server.py"); + + if (File.Exists(serverPy)) + { + status.IsAvailable = true; + status.Path = serverPath; + + // Try to get version + string versionFile = Path.Combine(serverPath, "server_version.txt"); + if (File.Exists(versionFile)) + { + status.Version = File.ReadAllText(versionFile).Trim(); + } + + status.Details = $"MCP Server found at {serverPath}"; + } + else + { + // Check for embedded server + if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath)) + { + status.IsAvailable = true; + status.Path = embeddedPath; + status.Details = "MCP Server available (embedded in package)"; + } + else + { + status.ErrorMessage = "MCP Server not found"; + status.Details = "Server will be installed automatically when needed"; + } + } + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}"; + } + + return status; + } + + protected bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + protected bool TryParseVersion(string version, out int major, out int minor) + { + major = 0; + minor = 0; + + try + { + var parts = version.Split('.'); + if (parts.Length >= 2) + { + return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor); + } + } + catch + { + // Ignore parsing errors + } + + return false; + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta new file mode 100644 index 00000000..4821e757 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44d715aedea2b8b41bf914433bbb2c49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs new file mode 100644 index 00000000..ea57d5ef --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs @@ -0,0 +1,232 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Windows-specific dependency detection + /// + public class WindowsPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "Windows"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths + var candidates = new[] + { + "python.exe", + "python3.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python313", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python312", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python311", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "Python313", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "Python312", "python.exe") + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'where' command + if (TryFindInPath("python.exe", out string pathResult) || + TryFindInPath("python3.exe", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths and PATH environment variable."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#windows"; + } + + public override string GetInstallationRecommendations() + { + return @"Windows Installation Recommendations: + +1. Python: Install from Microsoft Store or python.org + - Microsoft Store: Search for 'Python 3.12' or 'Python 3.13' + - Direct download: https://python.org/downloads/windows/ + +2. UV Package Manager: Install via PowerShell + - Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex"" + - Or download from: https://github.com/astral-sh/uv/releases + +3. MCP Server: Will be installed automatically by Unity MCP Bridge"; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "where", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output)) + { + // Take the first result + var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + if (lines.Length > 0) + { + fullPath = lines[0].Trim(); + return File.Exists(fullPath); + } + } + } + catch + { + // Ignore errors + } + + return false; + } + + private bool TryParseVersion(string version, out int major, out int minor) + { + return base.TryParseVersion(version, out major, out minor); + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta new file mode 100644 index 00000000..e7e53d7d --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 012345678901234abcdef0123456789a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs b/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs new file mode 100644 index 00000000..8e727efb --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs @@ -0,0 +1,297 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Shared helper for MCP client configuration management with sophisticated + /// logic for preserving existing configs and handling different client types + /// + public static class McpConfigurationHelper + { + private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig"; + + /// + /// Writes MCP configuration to the specified path using sophisticated logic + /// that preserves existing configuration and only writes when necessary + /// + public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null) + { + // 0) Respect explicit lock (hidden pref or UI toggle) + try + { + if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false)) + return "Skipped (locked)"; + } + catch { } + + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + + // Read existing config if it exists + string existingJson = "{}"; + if (File.Exists(configPath)) + { + try + { + existingJson = File.ReadAllText(configPath); + } + catch (Exception e) + { + Debug.LogWarning($"Error reading existing config: {e.Message}."); + } + } + + // Parse the existing JSON while preserving all properties + dynamic existingConfig; + try + { + if (string.IsNullOrWhiteSpace(existingJson)) + { + existingConfig = new JObject(); + } + else + { + existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject(); + } + } + catch + { + // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object + if (!string.IsNullOrWhiteSpace(existingJson)) + { + Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block."); + } + existingConfig = new JObject(); + } + + // Determine existing entry references (command/args) + string existingCommand = null; + string[] existingArgs = null; + bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode); + try + { + if (isVSCode) + { + existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); + existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject(); + } + else + { + existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); + existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject(); + } + } + catch { } + + // 1) Start from existing, only fill gaps (prefer trusted resolver) + string uvPath = ServerInstaller.FindUvPath(); + // Optionally trust existingCommand if it looks like uv/uv.exe + try + { + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + { + uvPath = existingCommand; + } + } + catch { } + if (uvPath == null) return "UV package manager not found. Please install UV first."; + string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + + // 2) Canonical args order + var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; + + // 3) Only write if changed + bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) + || !ArgsEqual(existingArgs, newArgs); + if (!changed) + { + return "Configured successfully"; // nothing to do + } + + // 4) Ensure containers exist and write back minimal changes + JObject existingRoot; + if (existingConfig is JObject eo) + existingRoot = eo; + else + existingRoot = JObject.FromObject(existingConfig); + + existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient); + + string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); + + McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); + + try + { + if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); + EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); + } + catch { } + + return "Configured successfully"; + } + + /// + /// Configures a Codex client with sophisticated TOML handling + /// + public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient) + { + try + { + if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false)) + return "Skipped (locked)"; + } + catch { } + + string existingToml = string.Empty; + if (File.Exists(configPath)) + { + try + { + existingToml = File.ReadAllText(configPath); + } + catch (Exception e) + { + Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}"); + existingToml = string.Empty; + } + } + + string existingCommand = null; + string[] existingArgs = null; + if (!string.IsNullOrWhiteSpace(existingToml)) + { + CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); + } + + string uvPath = ServerInstaller.FindUvPath(); + try + { + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + { + uvPath = existingCommand; + } + } + catch { } + + if (uvPath == null) + { + return "UV package manager not found. Please install UV first."; + } + + string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; + + bool changed = true; + if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null) + { + changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) + || !ArgsEqual(existingArgs, newArgs); + } + + if (!changed) + { + return "Configured successfully"; + } + + string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc); + string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock); + + McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml); + + try + { + if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); + EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); + } + catch { } + + return "Configured successfully"; + } + + /// + /// Validates UV binary by running --version command + /// + private static bool IsValidUvBinary(string path) + { + try + { + if (!File.Exists(path)) return false; + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = path, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = System.Diagnostics.Process.Start(psi); + if (p == null) return false; + if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; } + if (p.ExitCode != 0) return false; + string output = p.StandardOutput.ReadToEnd().Trim(); + return output.StartsWith("uv "); + } + catch { return false; } + } + + /// + /// Compares two string arrays for equality + /// + private static bool ArgsEqual(string[] a, string[] b) + { + if (a == null || b == null) return a == b; + if (a.Length != b.Length) return false; + for (int i = 0; i < a.Length; i++) + { + if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false; + } + return true; + } + + /// + /// Gets the appropriate config file path for the given MCP client based on OS + /// + public static string GetClientConfigPath(McpClient mcpClient) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return mcpClient.windowsConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return string.IsNullOrEmpty(mcpClient.macConfigPath) + ? mcpClient.linuxConfigPath + : mcpClient.macConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return mcpClient.linuxConfigPath; + } + else + { + return mcpClient.linuxConfigPath; // fallback + } + } + + /// + /// Creates the directory for the config file if it doesn't exist + /// + public static void EnsureConfigDirectoryExists(string configPath) + { + Directory.CreateDirectory(Path.GetDirectoryName(configPath)); + } + } +} diff --git a/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs.meta b/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs.meta new file mode 100644 index 00000000..17de56c8 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e45ac2a13b4c1ba468b8e3aa67b292ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs b/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs new file mode 100644 index 00000000..8e683965 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using UnityEngine; +using UnityEditor; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Shared helper for resolving Python server directory paths with support for + /// development mode, embedded servers, and installed packages + /// + public static class McpPathResolver + { + private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer"; + + /// + /// Resolves the Python server directory path with comprehensive logic + /// including development mode support and fallback mechanisms + /// + public static string FindPackagePythonDirectory(bool debugLogsEnabled = false) + { + string pythonDir = McpConfigFileHelper.ResolveServerSource(); + + try + { + // Only check dev paths if we're using a file-based package (development mode) + bool isDevelopmentMode = IsDevelopmentMode(); + if (isDevelopmentMode) + { + string currentPackagePath = Path.GetDirectoryName(Application.dataPath); + string[] devPaths = { + Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"), + Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"), + }; + + foreach (string devPath in devPaths) + { + if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py"))) + { + if (debugLogsEnabled) + { + Debug.Log($"Currently in development mode. Package: {devPath}"); + } + return devPath; + } + } + } + + // Resolve via shared helper (handles local registry and older fallback) only if dev override on + if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false)) + { + if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) + { + return embedded; + } + } + + // Log only if the resolved path does not actually contain server.py + if (debugLogsEnabled) + { + bool hasServer = false; + try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { } + if (!hasServer) + { + Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path"); + } + } + } + catch (Exception e) + { + Debug.LogError($"Error finding package path: {e.Message}"); + } + + return pythonDir; + } + + /// + /// Checks if the current Unity project is in development mode + /// (i.e., the package is referenced as a local file path in manifest.json) + /// + private static bool IsDevelopmentMode() + { + try + { + // Only treat as development if manifest explicitly references a local file path for the package + string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json"); + if (!File.Exists(manifestPath)) return false; + + string manifestContent = File.ReadAllText(manifestPath); + // Look specifically for our package dependency set to a file: URL + // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk + if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0) + { + int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase); + // Crude but effective: check for "file:" in the same line/value + if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0 + && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + catch + { + return false; + } + } + + /// + /// Gets the appropriate PATH prepend for the current platform when running external processes + /// + public static string GetPathPrepend() + { + if (Application.platform == RuntimePlatform.OSXEditor) + return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; + else if (Application.platform == RuntimePlatform.LinuxEditor) + return "/usr/local/bin:/usr/bin:/bin"; + return null; + } + } +} diff --git a/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs.meta b/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs.meta new file mode 100644 index 00000000..38f19973 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c76f0c7ff138ba4a952481e04bc3974 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs b/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs index 795256a7..031a6aed 100644 --- a/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs @@ -36,7 +36,7 @@ private static void InstallServerOnFirstLoad() catch (System.Exception ex) { Debug.LogError($"MCP-FOR-UNITY: Failed to install Python server: {ex.Message}"); - Debug.LogWarning("MCP-FOR-UNITY: You may need to manually install the Python server. Check the MCP for Unity Editor Window for instructions."); + Debug.LogWarning("MCP-FOR-UNITY: You may need to manually install the Python server. Check the MCP For Unity Window for instructions."); } } } diff --git a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs index 1342dc12..0e462945 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs @@ -12,7 +12,7 @@ public static class ServerPathResolver /// or common development locations. Returns true if found and sets srcPath to the folder /// containing server.py. /// - public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLegacyPackageId = true) + public static bool TryFindEmbeddedServerSource(out string srcPath) { // 1) Repo development layouts commonly used alongside this package try @@ -43,7 +43,7 @@ public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLe var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly); if (owner != null) { - if (TryResolveWithinPackage(owner, out srcPath, warnOnLegacyPackageId)) + if (TryResolveWithinPackage(owner, out srcPath)) { return true; } @@ -52,7 +52,7 @@ public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLe // Secondary: scan all registered packages locally foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages()) { - if (TryResolveWithinPackage(p, out srcPath, warnOnLegacyPackageId)) + if (TryResolveWithinPackage(p, out srcPath)) { return true; } @@ -65,7 +65,7 @@ public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLe { foreach (var pkg in list.Result) { - if (TryResolveWithinPackage(pkg, out srcPath, warnOnLegacyPackageId)) + if (TryResolveWithinPackage(pkg, out srcPath)) { return true; } @@ -99,24 +99,16 @@ public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLe return false; } - private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath, bool warnOnLegacyPackageId) + private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath) { const string CurrentId = "com.coplaydev.unity-mcp"; - const string LegacyId = "com.justinpbarnett.unity-mcp"; srcPath = null; - if (p == null || (p.name != CurrentId && p.name != LegacyId)) + if (p == null || p.name != CurrentId) { return false; } - if (warnOnLegacyPackageId && p.name == LegacyId) - { - Debug.LogWarning( - "MCP for Unity: Detected legacy package id 'com.justinpbarnett.unity-mcp'. " + - "Please update Packages/manifest.json to 'com.coplaydev.unity-mcp' to avoid future breakage."); - } - string packagePath = p.resolvedPath; // Preferred tilde folder (embedded but excluded from import) diff --git a/UnityMcpBridge/Editor/Setup.meta b/UnityMcpBridge/Editor/Setup.meta new file mode 100644 index 00000000..1157b1e9 --- /dev/null +++ b/UnityMcpBridge/Editor/Setup.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 600c9cb20c329d761bfa799158a87bac +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Setup/SetupWizard.cs b/UnityMcpBridge/Editor/Setup/SetupWizard.cs new file mode 100644 index 00000000..a97926ea --- /dev/null +++ b/UnityMcpBridge/Editor/Setup/SetupWizard.cs @@ -0,0 +1,150 @@ +using System; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Windows; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Setup +{ + /// + /// Handles automatic triggering of the setup wizard + /// + [InitializeOnLoad] + public static class SetupWizard + { + private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted"; + private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed"; + private static bool _hasCheckedThisSession = false; + + static SetupWizard() + { + // Skip in batch mode + if (Application.isBatchMode) + return; + + // Show setup wizard on package import + EditorApplication.delayCall += CheckSetupNeeded; + } + + /// + /// Check if setup wizard should be shown + /// + private static void CheckSetupNeeded() + { + if (_hasCheckedThisSession) + return; + + _hasCheckedThisSession = true; + + try + { + // Check if setup was already completed or dismissed in previous sessions + bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false); + bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false); + + // Only show setup wizard if it hasn't been completed or dismissed before + if (!(setupCompleted || setupDismissed)) + { + McpLog.Info("Package imported - showing setup wizard", always: false); + + var dependencyResult = DependencyManager.CheckAllDependencies(); + EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult); + } + else + { + McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false); + } + } + catch (Exception ex) + { + McpLog.Error($"Error checking setup status: {ex.Message}"); + } + } + + /// + /// Show the setup wizard window + /// + public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null) + { + try + { + dependencyResult ??= DependencyManager.CheckAllDependencies(); + SetupWizardWindow.ShowWindow(dependencyResult); + } + catch (Exception ex) + { + McpLog.Error($"Error showing setup wizard: {ex.Message}"); + } + } + + /// + /// Mark setup as completed + /// + public static void MarkSetupCompleted() + { + EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true); + McpLog.Info("Setup marked as completed"); + } + + /// + /// Mark setup as dismissed + /// + public static void MarkSetupDismissed() + { + EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true); + McpLog.Info("Setup marked as dismissed"); + } + + /// + /// Force show setup wizard (for manual invocation) + /// + [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)] + public static void ShowSetupWizardManual() + { + ShowSetupWizard(); + } + + /// + /// Check dependencies and show status + /// + [MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)] + public static void CheckDependencies() + { + var result = DependencyManager.CheckAllDependencies(); + + if (!result.IsSystemReady) + { + bool showWizard = EditorUtility.DisplayDialog( + "MCP for Unity - Dependencies", + $"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?", + "Open Setup Wizard", + "Close" + ); + + if (showWizard) + { + ShowSetupWizard(result); + } + } + else + { + EditorUtility.DisplayDialog( + "MCP for Unity - Dependencies", + "✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.", + "OK" + ); + } + } + + /// + /// Open MCP Client Configuration window + /// + [MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)] + public static void OpenClientConfiguration() + { + Windows.MCPForUnityEditorWindow.ShowWindow(); + } + } +} diff --git a/UnityMcpBridge/Editor/Setup/SetupWizard.cs.meta b/UnityMcpBridge/Editor/Setup/SetupWizard.cs.meta new file mode 100644 index 00000000..1a0e4e5f --- /dev/null +++ b/UnityMcpBridge/Editor/Setup/SetupWizard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 345678901234abcdef0123456789abcd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs b/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs new file mode 100644 index 00000000..7229be97 --- /dev/null +++ b/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs @@ -0,0 +1,726 @@ +using System; +using System.Linq; +using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Setup +{ + /// + /// Setup wizard window for guiding users through dependency installation + /// + public class SetupWizardWindow : EditorWindow + { + private DependencyCheckResult _dependencyResult; + private Vector2 _scrollPosition; + private int _currentStep = 0; + private McpClients _mcpClients; + private int _selectedClientIndex = 0; + + private readonly string[] _stepTitles = { + "Setup", + "Configure", + "Complete" + }; + + public static void ShowWindow(DependencyCheckResult dependencyResult = null) + { + var window = GetWindow("MCP for Unity Setup"); + window.minSize = new Vector2(500, 400); + window.maxSize = new Vector2(800, 600); + window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies(); + window.Show(); + } + + private void OnEnable() + { + if (_dependencyResult == null) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + + _mcpClients = new McpClients(); + + // Check client configurations on startup + foreach (var client in _mcpClients.clients) + { + CheckClientConfiguration(client); + } + } + + private void OnGUI() + { + DrawHeader(); + DrawProgressBar(); + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + + switch (_currentStep) + { + case 0: DrawSetupStep(); break; + case 1: DrawConfigureStep(); break; + case 2: DrawCompleteStep(); break; + } + + EditorGUILayout.EndScrollView(); + + DrawFooter(); + } + + private void DrawHeader() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}"); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // Step title + var titleStyle = new GUIStyle(EditorStyles.largeLabel) + { + fontSize = 16, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle); + EditorGUILayout.Space(); + } + + private void DrawProgressBar() + { + var rect = EditorGUILayout.GetControlRect(false, 4); + var progress = (_currentStep + 1) / (float)_stepTitles.Length; + EditorGUI.ProgressBar(rect, progress, ""); + EditorGUILayout.Space(); + } + + private void DrawSetupStep() + { + // Welcome section + DrawSectionTitle("MCP for Unity Setup"); + + EditorGUILayout.LabelField( + "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + // Dependency check section + EditorGUILayout.BeginHorizontal(); + DrawSectionTitle("System Check", 14); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20))) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + EditorGUILayout.EndHorizontal(); + + // Show simplified dependency status + foreach (var dep in _dependencyResult.Dependencies) + { + DrawSimpleDependencyStatus(dep); + } + + // Overall status and installation guidance + EditorGUILayout.Space(); + if (!_dependencyResult.IsSystemReady) + { + // Only show critical warnings when dependencies are actually missing + EditorGUILayout.HelpBox( + "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.", + MessageType.Warning + ); + + EditorGUILayout.Space(); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + DrawErrorStatus("Installation Required"); + + var recommendations = DependencyManager.GetInstallationRecommendations(); + EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel); + + EditorGUILayout.Space(); + if (GUILayout.Button("Open Installation Links", GUILayout.Height(25))) + { + OpenInstallationUrls(); + } + EditorGUILayout.EndVertical(); + } + else + { + DrawSuccessStatus("System Ready"); + EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel); + } + } + + + + private void DrawCompleteStep() + { + DrawSectionTitle("Setup Complete"); + + // Refresh dependency check with caching to avoid heavy operations on every repaint + if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + + if (_dependencyResult.IsSystemReady) + { + DrawSuccessStatus("MCP for Unity Ready!"); + + EditorGUILayout.HelpBox( + "🎉 MCP for Unity is now set up and ready to use!\n\n" + + "• Dependencies verified\n" + + "• MCP server ready\n" + + "• Client configuration accessible", + MessageType.Info + ); + + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Documentation", GUILayout.Height(30))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp"); + } + if (GUILayout.Button("Client Settings", GUILayout.Height(30))) + { + Windows.MCPForUnityEditorWindow.ShowWindow(); + } + EditorGUILayout.EndHorizontal(); + } + else + { + DrawErrorStatus("Setup Incomplete - Package Non-Functional"); + + EditorGUILayout.HelpBox( + "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" + + "Install ALL required dependencies before the package will function.", + MessageType.Error + ); + + var missingDeps = _dependencyResult.GetMissingRequired(); + if (missingDeps.Count > 0) + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel); + foreach (var dep in missingDeps) + { + EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label); + } + } + + EditorGUILayout.Space(); + if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) + { + _currentStep = 0; + } + } + } + + // Helper methods for consistent UI components + private void DrawSectionTitle(string title, int fontSize = 16) + { + var titleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = fontSize, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(title, titleStyle); + EditorGUILayout.Space(); + } + + private void DrawSuccessStatus(string message) + { + var originalColor = GUI.color; + GUI.color = Color.green; + EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel); + GUI.color = originalColor; + EditorGUILayout.Space(); + } + + private void DrawErrorStatus(string message) + { + var originalColor = GUI.color; + GUI.color = Color.red; + EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel); + GUI.color = originalColor; + EditorGUILayout.Space(); + } + + private void DrawSimpleDependencyStatus(DependencyStatus dep) + { + EditorGUILayout.BeginHorizontal(); + + var statusIcon = dep.IsAvailable ? "✓" : "✗"; + var statusColor = dep.IsAvailable ? Color.green : Color.red; + + var originalColor = GUI.color; + GUI.color = statusColor; + GUILayout.Label(statusIcon, GUILayout.Width(20)); + EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel); + GUI.color = originalColor; + + if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage)) + { + EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel); + } + + EditorGUILayout.EndHorizontal(); + } + + private void DrawConfigureStep() + { + DrawSectionTitle("AI Client Configuration"); + + // Check dependencies first (with caching to avoid heavy operations on every repaint) + if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + if (!_dependencyResult.IsSystemReady) + { + DrawErrorStatus("Cannot Configure - System Requirements Not Met"); + + EditorGUILayout.HelpBox( + "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.", + MessageType.Warning + ); + + if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) + { + _currentStep = 0; + } + return; + } + + EditorGUILayout.LabelField( + "Configure your AI assistants to work with Unity. Select a client below to set it up:", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + // Client selection and configuration + if (_mcpClients.clients.Count > 0) + { + // Client selector dropdown + string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray(); + EditorGUI.BeginChangeCheck(); + _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames); + if (EditorGUI.EndChangeCheck()) + { + _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1); + // Refresh client status when selection changes + CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]); + } + + EditorGUILayout.Space(); + + var selectedClient = _mcpClients.clients[_selectedClientIndex]; + DrawClientConfigurationInWizard(selectedClient); + + EditorGUILayout.Space(); + + // Batch configuration option + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel); + EditorGUILayout.LabelField( + "Automatically configure all detected AI clients at once:", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30))) + { + ConfigureAllClientsInWizard(); + } + EditorGUILayout.EndVertical(); + } + else + { + EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info); + } + + EditorGUILayout.Space(); + EditorGUILayout.HelpBox( + "💡 You might need to restart your AI client after configuring.", + MessageType.Info + ); + } + + private void DrawFooter() + { + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + + // Back button + GUI.enabled = _currentStep > 0; + if (GUILayout.Button("Back", GUILayout.Width(60))) + { + _currentStep--; + } + + GUILayout.FlexibleSpace(); + + // Skip button + if (GUILayout.Button("Skip", GUILayout.Width(60))) + { + bool dismiss = EditorUtility.DisplayDialog( + "Skip Setup", + "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" + + "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)", + "Skip Anyway", + "Cancel" + ); + + if (dismiss) + { + SetupWizard.MarkSetupDismissed(); + Close(); + } + } + + // Next/Done button + GUI.enabled = true; + string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next"; + + if (GUILayout.Button(buttonText, GUILayout.Width(80))) + { + if (_currentStep == _stepTitles.Length - 1) + { + SetupWizard.MarkSetupCompleted(); + Close(); + } + else + { + _currentStep++; + } + } + + GUI.enabled = true; + EditorGUILayout.EndHorizontal(); + } + + private void DrawClientConfigurationInWizard(McpClient client) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // Show current status + var statusColor = GetClientStatusColor(client); + var originalColor = GUI.color; + GUI.color = statusColor; + EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label); + GUI.color = originalColor; + + EditorGUILayout.Space(); + + // Configuration buttons + EditorGUILayout.BeginHorizontal(); + + if (client.mcpType == McpTypes.ClaudeCode) + { + // Special handling for Claude Code + bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); + if (claudeAvailable) + { + bool isConfigured = client.status == McpStatus.Configured; + string buttonText = isConfigured ? "Unregister" : "Register"; + if (GUILayout.Button($"{buttonText} with Claude Code")) + { + if (isConfigured) + { + UnregisterFromClaudeCode(client); + } + else + { + RegisterWithClaudeCode(client); + } + } + } + else + { + EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning); + if (GUILayout.Button("Open Claude Code Website")) + { + Application.OpenURL("https://claude.ai/download"); + } + } + } + else + { + // Standard client configuration + if (GUILayout.Button($"Configure {client.name}")) + { + ConfigureClientInWizard(client); + } + + if (GUILayout.Button("Manual Setup")) + { + ShowManualSetupInWizard(client); + } + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + + private Color GetClientStatusColor(McpClient client) + { + return client.status switch + { + McpStatus.Configured => Color.green, + McpStatus.Running => Color.green, + McpStatus.Connected => Color.green, + McpStatus.IncorrectPath => Color.yellow, + McpStatus.CommunicationError => Color.yellow, + McpStatus.NoResponse => Color.yellow, + _ => Color.red + }; + } + + private void ConfigureClientInWizard(McpClient client) + { + try + { + string result = PerformClientConfiguration(client); + + EditorUtility.DisplayDialog( + $"{client.name} Configuration", + result, + "OK" + ); + + // Refresh client status + CheckClientConfiguration(client); + Repaint(); + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog( + "Configuration Error", + $"Failed to configure {client.name}: {ex.Message}", + "OK" + ); + } + } + + private void ConfigureAllClientsInWizard() + { + int successCount = 0; + int totalCount = _mcpClients.clients.Count; + + foreach (var client in _mcpClients.clients) + { + try + { + if (client.mcpType == McpTypes.ClaudeCode) + { + if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured) + { + RegisterWithClaudeCode(client); + successCount++; + } + else if (client.status == McpStatus.Configured) + { + successCount++; // Already configured + } + } + else + { + string result = PerformClientConfiguration(client); + if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase)) + { + successCount++; + } + } + + CheckClientConfiguration(client); + } + catch (System.Exception ex) + { + McpLog.Error($"Failed to configure {client.name}: {ex.Message}"); + } + } + + EditorUtility.DisplayDialog( + "Batch Configuration Complete", + $"Successfully configured {successCount} out of {totalCount} clients.\n\n" + + "Restart your AI clients for changes to take effect.", + "OK" + ); + + Repaint(); + } + + private void RegisterWithClaudeCode(McpClient client) + { + try + { + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + string claudePath = ExecPath.ResolveClaude(); + string uvPath = ExecPath.ResolveUv() ?? "uv"; + + string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; + + if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend())) + { + if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase)) + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK"); + } + else + { + throw new System.Exception($"Registration failed: {stderr}"); + } + } + else + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK"); + } + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK"); + } + } + + private void UnregisterFromClaudeCode(McpClient client) + { + try + { + string claudePath = ExecPath.ResolveClaude(); + if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend())) + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK"); + } + else + { + throw new System.Exception($"Unregistration failed: {stderr}"); + } + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK"); + } + } + + private string PerformClientConfiguration(McpClient client) + { + // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + + if (string.IsNullOrEmpty(pythonDir)) + { + return "Manual configuration required - Python server directory not found."; + } + + McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); + return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); + } + + private void ShowManualSetupInWizard(McpClient client) + { + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + string uvPath = ServerInstaller.FindUvPath(); + + if (string.IsNullOrEmpty(uvPath)) + { + EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK"); + return; + } + + // Build manual configuration using the sophisticated helper logic + string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); + string manualConfig; + + if (result == "Configured successfully") + { + // Read back the configuration that was written + try + { + manualConfig = System.IO.File.ReadAllText(configPath); + } + catch + { + manualConfig = "Configuration written successfully, but could not read back for display."; + } + } + else + { + manualConfig = $"Configuration failed: {result}"; + } + + EditorUtility.DisplayDialog( + $"Manual Setup - {client.name}", + $"Configuration file location:\n{configPath}\n\n" + + $"Configuration result:\n{manualConfig}", + "OK" + ); + } + + private void CheckClientConfiguration(McpClient client) + { + // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic + try + { + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + if (System.IO.File.Exists(configPath)) + { + client.configStatus = "Configured"; + client.status = McpStatus.Configured; + } + else + { + client.configStatus = "Not Configured"; + client.status = McpStatus.NotConfigured; + } + } + catch + { + client.configStatus = "Error"; + client.status = McpStatus.Error; + } + } + + private void OpenInstallationUrls() + { + var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls(); + + bool openPython = EditorUtility.DisplayDialog( + "Open Installation URLs", + "Open Python installation page?", + "Yes", + "No" + ); + + if (openPython) + { + Application.OpenURL(pythonUrl); + } + + bool openUV = EditorUtility.DisplayDialog( + "Open Installation URLs", + "Open UV installation page?", + "Yes", + "No" + ); + + if (openUV) + { + Application.OpenURL(uvUrl); + } + } + } +} diff --git a/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs.meta b/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs.meta new file mode 100644 index 00000000..5361de3d --- /dev/null +++ b/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45678901234abcdef0123456789abcde +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index a02193e6..ed70181b 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -45,10 +45,10 @@ public class MCPForUnityEditorWindow : EditorWindow // UI state private int selectedClientIndex = 0; - [MenuItem("Window/MCP for Unity")] + [MenuItem("Window/MCP For Unity")] public static void ShowWindow() { - GetWindow("MCP for Unity"); + GetWindow("MCP For Unity"); } private void OnEnable() @@ -235,7 +235,7 @@ private void DrawHeader() GUI.Label( new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height), - "MCP for Unity Editor", + "MCP For Unity", titleStyle ); @@ -381,12 +381,12 @@ private void DrawServerStatusSection() bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RepairPythonEnvironment(); if (ok) { - EditorUtility.DisplayDialog("MCP for Unity", "Python environment repaired.", "OK"); + EditorUtility.DisplayDialog("MCP For Unity", "Python environment repaired.", "OK"); UpdatePythonServerInstallationStatus(); } else { - EditorUtility.DisplayDialog("MCP for Unity", "Repair failed. Please check Console for details.", "OK"); + EditorUtility.DisplayDialog("MCP For Unity", "Repair failed. Please check Console for details.", "OK"); } } } @@ -1099,170 +1099,6 @@ private void ToggleUnityBridge() Repaint(); } - private static bool IsValidUv(string path) - { - return !string.IsNullOrEmpty(path) - && System.IO.Path.IsPathRooted(path) - && System.IO.File.Exists(path); - } - - private static bool ValidateUvBinarySafe(string path) - { - try - { - if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false; - var psi = new System.Diagnostics.ProcessStartInfo - { - FileName = path, - Arguments = "--version", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using var p = System.Diagnostics.Process.Start(psi); - if (p == null) return false; - if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; } - if (p.ExitCode != 0) return false; - string output = p.StandardOutput.ReadToEnd().Trim(); - return output.StartsWith("uv "); - } - catch { return false; } - } - - private static bool ArgsEqual(string[] a, string[] b) - { - if (a == null || b == null) return a == b; - if (a.Length != b.Length) return false; - for (int i = 0; i < a.Length; i++) - { - if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false; - } - return true; - } - - private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null) - { - // 0) Respect explicit lock (hidden pref or UI toggle) - try { if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { } - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - - // Read existing config if it exists - string existingJson = "{}"; - if (File.Exists(configPath)) - { - try - { - existingJson = File.ReadAllText(configPath); - } - catch (Exception e) - { - UnityEngine.Debug.LogWarning($"Error reading existing config: {e.Message}."); - } - } - - // Parse the existing JSON while preserving all properties - dynamic existingConfig; - try - { - if (string.IsNullOrWhiteSpace(existingJson)) - { - existingConfig = new Newtonsoft.Json.Linq.JObject(); - } - else - { - existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new Newtonsoft.Json.Linq.JObject(); - } - } - catch - { - // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object - if (!string.IsNullOrWhiteSpace(existingJson)) - { - UnityEngine.Debug.LogWarning("UnityMCP: VSCode mcp.json could not be parsed; rewriting servers block."); - } - existingConfig = new Newtonsoft.Json.Linq.JObject(); - } - - // Determine existing entry references (command/args) - string existingCommand = null; - string[] existingArgs = null; - bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode); - try - { - if (isVSCode) - { - existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); - existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject(); - } - else - { - existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); - existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject(); - } - } - catch { } - - // 1) Start from existing, only fill gaps (prefer trusted resolver) - string uvPath = ServerInstaller.FindUvPath(); - // Optionally trust existingCommand if it looks like uv/uv.exe - try - { - var name = System.IO.Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); - if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand)) - { - uvPath = existingCommand; - } - } - catch { } - if (uvPath == null) return "UV package manager not found. Please install UV first."; - string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); - - // 2) Canonical args order - var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; - - // 3) Only write if changed - bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) - || !ArgsEqual(existingArgs, newArgs); - if (!changed) - { - return "Configured successfully"; // nothing to do - } - - // 4) Ensure containers exist and write back minimal changes - JObject existingRoot; - if (existingConfig is JObject eo) - existingRoot = eo; - else - existingRoot = JObject.FromObject(existingConfig); - - existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient); - - string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); - - McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); - - try - { - if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); - UnityEditor.EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); - } - catch { } - - return "Configured successfully"; - } - - private void ShowManualConfigurationInstructions( - string configPath, - McpClient mcpClient - ) - { - mcpClient.SetStatus(McpStatus.Error, "Manual configuration required"); - - ShowManualInstructionsWindow(configPath, mcpClient); - } - // New method to show manual instructions without changing status private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient) { @@ -1284,124 +1120,21 @@ private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient private string FindPackagePythonDirectory() { - string pythonDir = McpConfigFileHelper.ResolveServerSource(); - - try - { - // Only check dev paths if we're using a file-based package (development mode) - bool isDevelopmentMode = IsDevelopmentMode(); - if (isDevelopmentMode) - { - string currentPackagePath = Path.GetDirectoryName(Application.dataPath); - string[] devPaths = { - Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"), - Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"), - }; - - foreach (string devPath in devPaths) - { - if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py"))) - { - if (debugLogsEnabled) - { - UnityEngine.Debug.Log($"Currently in development mode. Package: {devPath}"); - } - return devPath; - } - } - } - - // Resolve via shared helper (handles local registry and older fallback) only if dev override on - if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false)) - { - if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) - { - return embedded; - } - } - - // Log only if the resolved path does not actually contain server.py - if (debugLogsEnabled) - { - bool hasServer = false; - try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { } - if (!hasServer) - { - UnityEngine.Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path"); - } - } - } - catch (Exception e) - { - UnityEngine.Debug.LogError($"Error finding package path: {e.Message}"); - } - - return pythonDir; - } - - private bool IsDevelopmentMode() - { - try - { - // Only treat as development if manifest explicitly references a local file path for the package - string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json"); - if (!File.Exists(manifestPath)) return false; - - string manifestContent = File.ReadAllText(manifestPath); - // Look specifically for our package dependency set to a file: URL - // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk - if (manifestContent.IndexOf("\"com.justinpbarnett.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0) - { - int idx = manifestContent.IndexOf("com.justinpbarnett.unity-mcp", StringComparison.OrdinalIgnoreCase); - // Crude but effective: check for "file:" in the same line/value - if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0 - && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; - } - catch - { - return false; - } + // Use shared helper for consistent path resolution across both windows + return McpPathResolver.FindPackagePythonDirectory(debugLogsEnabled); } private string ConfigureMcpClient(McpClient mcpClient) { try { - // Determine the config file path based on OS - string configPath; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - configPath = mcpClient.windowsConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.OSX) - ) - { - configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) - ? mcpClient.linuxConfigPath - : mcpClient.macConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ) - { - configPath = mcpClient.linuxConfigPath; - } - else - { - return "Unsupported OS"; - } + // Use shared helper for consistent config path resolution + string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); // Create directory if it doesn't exist - Directory.CreateDirectory(Path.GetDirectoryName(configPath)); + McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); - // Find the server.py file location using the same logic as FindPackagePythonDirectory + // Find the server.py file location using shared helper string pythonDir = FindPackagePythonDirectory(); if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) @@ -1411,8 +1144,8 @@ private string ConfigureMcpClient(McpClient mcpClient) } string result = mcpClient.mcpType == McpTypes.Codex - ? ConfigureCodexClient(pythonDir, configPath, mcpClient) - : WriteToConfig(pythonDir, configPath, mcpClient); + ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) + : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); // Update the client status after successful configuration if (result == "Configured successfully") @@ -1453,116 +1186,6 @@ private string ConfigureMcpClient(McpClient mcpClient) } } - private string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient) - { - try { if (EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { } - - string existingToml = string.Empty; - if (File.Exists(configPath)) - { - try - { - existingToml = File.ReadAllText(configPath); - } - catch (Exception e) - { - if (debugLogsEnabled) - { - UnityEngine.Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}"); - } - existingToml = string.Empty; - } - } - - string existingCommand = null; - string[] existingArgs = null; - if (!string.IsNullOrWhiteSpace(existingToml)) - { - CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); - } - - string uvPath = ServerInstaller.FindUvPath(); - try - { - var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); - if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand)) - { - uvPath = existingCommand; - } - } - catch { } - - if (uvPath == null) - { - return "UV package manager not found. Please install UV first."; - } - - string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); - var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; - - bool changed = true; - if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null) - { - changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) - || !ArgsEqual(existingArgs, newArgs); - } - - if (!changed) - { - return "Configured successfully"; - } - - string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc); - string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock); - - McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml); - - try - { - if (IsValidUv(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); - EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); - } - catch { } - - return "Configured successfully"; - } - - private void ShowCursorManualConfigurationInstructions( - string configPath, - McpClient mcpClient - ) - { - mcpClient.SetStatus(McpStatus.Error, "Manual configuration required"); - - // Get the Python directory path using Package Manager API - string pythonDir = FindPackagePythonDirectory(); - - // Create the manual configuration message - string uvPath = FindUvPath(); - if (uvPath == null) - { - UnityEngine.Debug.LogError("UV package manager not found. Cannot configure manual setup."); - return; - } - - McpConfig jsonConfig = new() - { - mcpServers = new McpConfigServers - { - unityMCP = new McpConfigServer - { - command = uvPath, - args = new[] { "run", "--directory", pythonDir, "server.py" }, - }, - }, - }; - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); - - ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); - } - private void LoadValidationLevelSetting() { string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); @@ -1601,12 +1224,6 @@ private string GetValidationLevelDescription(int index) }; } - public static string GetCurrentValidationLevel() - { - string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); - return savedLevel; - } - private void CheckMcpConfiguration(McpClient mcpClient) { try @@ -1618,30 +1235,8 @@ private void CheckMcpConfiguration(McpClient mcpClient) return; } - string configPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - configPath = mcpClient.windowsConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.OSX) - ) - { - configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) - ? mcpClient.linuxConfigPath - : mcpClient.macConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ) - { - configPath = mcpClient.linuxConfigPath; - } - else - { - mcpClient.SetStatus(McpStatus.UnsupportedOS); - return; - } + // Use shared helper for consistent config path resolution + string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); if (!File.Exists(configPath)) { @@ -1711,8 +1306,8 @@ private void CheckMcpConfiguration(McpClient mcpClient) try { string rewriteResult = mcpClient.mcpType == McpTypes.Codex - ? ConfigureCodexClient(pythonDir, configPath, mcpClient) - : WriteToConfig(pythonDir, configPath, mcpClient); + ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) + : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); if (rewriteResult == "Configured successfully") { if (debugLogsEnabled) diff --git a/UnityMcpBridge/package.json b/UnityMcpBridge/package.json index 3ec3f985..b239aca5 100644 --- a/UnityMcpBridge/package.json +++ b/UnityMcpBridge/package.json @@ -2,7 +2,7 @@ "name": "com.coplaydev.unity-mcp", "version": "4.1.1", "displayName": "MCP for Unity", - "description": "A bridge that connects an LLM to Unity via the MCP (Model Context Protocol). This allows MCP Clients like Claude Desktop or Cursor to directly control your Unity Editor.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4", + "description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4", "unity": "2021.3", "documentationUrl": "https://github.com/CoplayDev/unity-mcp", "licensesUrl": "https://github.com/CoplayDev/unity-mcp/blob/main/LICENSE", From 5488af2c992aa47a6b66f2e02f08e585dd5c93f9 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 3 Oct 2025 18:53:09 -0400 Subject: [PATCH 3/7] Make it easier to add tools (#301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add a decorate that wraps around the `mcp.tool` decorator. This will allow us to more easily collect tools * Register tools that's defined in the tools folder * Update Python tools to use new decorator * Convert script_apply_edits tool * Convert last remaining tools with new decorator * Create an attribute so we can identify tools via Reflection * Add attribute to all C# tools * Use reflection to load tools * Initialize command registry to load tools at startup * Update tests * Move Dev docs to docs folder * Add docs for adding custom tools * Update function docs for Python decorator * Add working example of adding a screenshot tool * docs: update relative links in README files Updated the relative links in both README-DEV.md and README-DEV-zh.md to use direct filenames instead of paths relative to the docs directory, improving link correctness when files are accessed from the root directory. * docs: update telemetry documentation path reference Updated the link to TELEMETRY.md in README.md to point to the new docs/ directory location to ensure users can access the telemetry documentation correctly. Also moved the TELEMETRY.md file to the docs/ directory as part of the documentation restructuring. * rename CursorHelp.md to docs/CURSOR_HELP.md Moved the CursorHelp.md file to the docs directory to better organize documentation files and improve project structure. * docs: update CUSTOM_TOOLS.md with improved tool naming documentation and path corrections - Clarified that the `name` argument in `@mcp_for_unity_tool` decorator is optional and defaults to the function name - Added documentation about using all FastMCP `mcp.tool` function decorator options - Updated class naming documentation to mention snake_case conversion by default - Corrected Python file path from `tools/screenshot_tool.py` to `UnityMcpServer~/src/tools/screenshot_tool.py` - Enhanced documentation for tool discovery and usage examples * docs: restructure development documentation and add custom tools guide Rearranged the development section in README.md to better organize the documentation flow. Added a dedicated section for "Adding Custom Tools" with a link to the new CUSTOM_TOOLS.md file, and renamed the previous "For Developers" section to "Contributing to the Project" to better reflect its content. This improves discoverability and organization of the development setup documentation. * docs: update developer documentation and add README links - Added links to developer READMEs in CUSTOM_TOOLS.md to guide users to the appropriate documentation - Fixed typo in README-DEV.md ("roote" → "root") for improved clarity - These changes improve the developer experience by providing better documentation navigation and correcting technical inaccuracies * feat(tools): enhance tool registration with wrapped function assignment Updated the tool registration process to properly chain the mcp.tool decorator and telemetry wrapper, ensuring the wrapped function is correctly assigned to tool_info['func'] for proper tool execution and telemetry tracking. This change improves the reliability of tool registration and monitoring. * Remove AI generated code that was never used... * feat: Rebuild MCP server installation with embedded source Refactored the server repair logic to implement a full rebuild of the MCP server installation using the embedded source. The new RebuildMcpServer method now: - Uses embedded server source instead of attempting repair of existing installation - Deletes the entire existing server directory before re-copying - Handles UV process cleanup for the target path - Simplifies the installation flow by removing the complex Python environment repair logic - Maintains the same installation behavior but with a cleaner, more reliable rebuild approach This change improves reliability of server installations by ensuring a clean slate rebuild rather than attempting to repair potentially corrupted environments. * Add the rebuild server step * docs: clarify tool description field requirements and client compatibility * fix: move initialization flag after tool discovery to prevent race conditions * refactor: remove redundant TryParseVersion overrides in platform detectors * refactor: remove duplicate UV validation code from platform detectors * Update UnityMcpBridge/Editor/Tools/CommandRegistry.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: replace WriteToConfig reflection with direct McpConfigurationHelper call --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- README-zh.md | 10 +- README.md | 10 +- .../WriteToConfigTests.cs | 39 +- .../WriteToConfigTests.cs.meta | 0 .../EditMode/Tools/CommandRegistryTests.cs | 50 +- .../Editor/Dependencies/DependencyManager.cs | 161 --- .../LinuxPlatformDetector.cs | 41 - .../MacOSPlatformDetector.cs | 41 - .../WindowsPlatformDetector.cs | 41 - .../Editor/Helpers/ServerInstaller.cs | 110 +- UnityMcpBridge/Editor/MCPForUnityBridge.cs | 1 + .../Editor/Tools/CommandRegistry.cs | 140 ++- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 1 + UnityMcpBridge/Editor/Tools/ManageEditor.cs | 1 + .../Editor/Tools/ManageGameObject.cs | 1 + UnityMcpBridge/Editor/Tools/ManageScene.cs | 1 + UnityMcpBridge/Editor/Tools/ManageScript.cs | 1 + UnityMcpBridge/Editor/Tools/ManageShader.cs | 1 + .../Editor/Tools/McpForUnityToolAttribute.cs | 37 + .../Tools/McpForUnityToolAttribute.cs.meta | 11 + .../Editor/Tools/MenuItems/ManageMenuItem.cs | 1 + .../Editor/Tools/Prefabs/ManagePrefabs.cs | 1 + UnityMcpBridge/Editor/Tools/ReadConsole.cs | 1 + .../Editor/Windows/MCPForUnityEditorWindow.cs | 12 +- UnityMcpBridge/README.md | 2 +- .../UnityMcpServer~/src/registry/__init__.py | 14 + .../src/registry/tool_registry.py | 51 + .../UnityMcpServer~/src/tools/__init__.py | 77 +- .../UnityMcpServer~/src/tools/manage_asset.py | 135 ++- .../src/tools/manage_editor.py | 105 +- .../src/tools/manage_gameobject.py | 271 +++-- .../src/tools/manage_menu_item.py | 63 +- .../src/tools/manage_prefabs.py | 105 +- .../UnityMcpServer~/src/tools/manage_scene.py | 103 +- .../src/tools/manage_script.py | 1025 ++++++++--------- .../src/tools/manage_script_edits.py | 968 ---------------- .../src/tools/manage_shader.py | 111 +- .../UnityMcpServer~/src/tools/read_console.py | 143 ++- .../src/tools/resource_tools.py | 492 ++++---- .../src/tools/script_apply_edits.py | 966 ++++++++++++++++ CursorHelp.md => docs/CURSOR_HELP.md | 0 docs/CUSTOM_TOOLS.md | 287 +++++ README-DEV-zh.md => docs/README-DEV-zh.md | 0 README-DEV.md => docs/README-DEV.md | 2 +- TELEMETRY.md => docs/TELEMETRY.md | 0 45 files changed, 2894 insertions(+), 2739 deletions(-) rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/{Windows => Helpers}/WriteToConfigTests.cs (87%) rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/{Windows => Helpers}/WriteToConfigTests.cs.meta (100%) create mode 100644 UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs create mode 100644 UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs.meta create mode 100644 UnityMcpBridge/UnityMcpServer~/src/registry/__init__.py create mode 100644 UnityMcpBridge/UnityMcpServer~/src/registry/tool_registry.py delete mode 100644 UnityMcpBridge/UnityMcpServer~/src/tools/manage_script_edits.py create mode 100644 UnityMcpBridge/UnityMcpServer~/src/tools/script_apply_edits.py rename CursorHelp.md => docs/CURSOR_HELP.md (100%) create mode 100644 docs/CUSTOM_TOOLS.md rename README-DEV-zh.md => docs/README-DEV-zh.md (100%) rename README-DEV.md => docs/README-DEV.md (98%) rename TELEMETRY.md => docs/TELEMETRY.md (100%) diff --git a/README-zh.md b/README-zh.md index c5f0860b..62c6ae01 100644 --- a/README-zh.md +++ b/README-zh.md @@ -270,7 +270,11 @@ claude mcp add UnityMCP -- "C:/Users/USERNAME/AppData/Local/Microsoft/WinGet/Lin ## 开发和贡献 🛠️ -### 开发者 +### 添加自定义工具 + +MCP for Unity 使用与 Unity 的 C# 脚本绑定的 Python MCP 服务器来实现工具功能。如果您想使用自己的工具扩展功能,请参阅 **[CUSTOM_TOOLS.md](docs/CUSTOM_TOOLS.md)** 了解如何操作。 + +### 贡献项目 如果您正在为 MCP for Unity 做贡献或想要测试核心更改,我们有开发工具来简化您的工作流程: @@ -278,7 +282,7 @@ claude mcp add UnityMCP -- "C:/Users/USERNAME/AppData/Local/Microsoft/WinGet/Lin - **自动备份系统**:具有简单回滚功能的安全测试 - **热重载工作流程**:核心开发的快速迭代周期 -📖 **查看 [README-DEV.md](README-DEV.md)** 获取完整的开发设置和工作流程文档。 +📖 **查看 [README-DEV.md](docs/README-DEV.md)** 获取完整的开发设置和工作流程文档。 ### 贡献 🤝 @@ -299,7 +303,7 @@ Unity MCP 包含**注重隐私的匿名遥测**来帮助我们改进产品。我 - **🔒 匿名**:仅随机 UUID,无个人数据 - **🚫 轻松退出**:设置 `DISABLE_TELEMETRY=true` 环境变量 -- **📖 透明**:查看 [TELEMETRY.md](TELEMETRY.md) 获取完整详情 +- **📖 透明**:查看 [TELEMETRY.md](docs/TELEMETRY.md) 获取完整详情 您的隐私对我们很重要。所有遥测都是可选的,旨在尊重您的工作流程。 diff --git a/README.md b/README.md index f06217f4..074b0dc6 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,11 @@ On Windows, set `command` to the absolute shim, e.g. `C:\\Users\\YOU\\AppData\\L ## Development & Contributing 🛠️ -### For Developers +### Adding Custom Tools + +MCP for Unity uses a Python MCP Server tied with Unity's C# scripts for tools. If you'd like to extend the functionality with your own tools, learn how to do so in **[CUSTOM_TOOLS.md](docs/CUSTOM_TOOLS.md)**. + +### Contributing to the Project If you're contributing to MCP for Unity or want to test core changes, we have development tools to streamline your workflow: @@ -281,7 +285,7 @@ If you're contributing to MCP for Unity or want to test core changes, we have de - **Automatic Backup System**: Safe testing with easy rollback capabilities - **Hot Reload Workflow**: Fast iteration cycle for core development -📖 **See [README-DEV.md](README-DEV.md)** for complete development setup and workflow documentation. +📖 **See [README-DEV.md](docs/README-DEV.md)** for complete development setup and workflow documentation. ### Contributing 🤝 @@ -302,7 +306,7 @@ Unity MCP includes **privacy-focused, anonymous telemetry** to help us improve t - **🔒 Anonymous**: Random UUIDs only, no personal data - **🚫 Easy opt-out**: Set `DISABLE_TELEMETRY=true` environment variable -- **📖 Transparent**: See [TELEMETRY.md](TELEMETRY.md) for full details +- **📖 Transparent**: See [TELEMETRY.md](docs/TELEMETRY.md) for full details Your privacy matters to us. All telemetry is optional and designed to respect your workflow. diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs similarity index 87% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs index 3fd77088..88f4118d 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs @@ -1,17 +1,14 @@ using System; using System.Diagnostics; using System.IO; -using System.Reflection; using System.Runtime.InteropServices; using Newtonsoft.Json.Linq; using NUnit.Framework; using UnityEditor; -using UnityEngine; -using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Windows; -namespace MCPForUnityTests.Editor.Windows +namespace MCPForUnityTests.Editor.Helpers { public class WriteToConfigTests { @@ -68,7 +65,7 @@ public void TearDown() public void AddsEnvAndDisabledFalse_ForWindsurf() { var configPath = Path.Combine(_tempRoot, "windsurf.json"); - WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path"); + WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path"); var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf }; InvokeWriteToConfig(configPath, client); @@ -85,7 +82,7 @@ public void AddsEnvAndDisabledFalse_ForWindsurf() public void AddsEnvAndDisabledFalse_ForKiro() { var configPath = Path.Combine(_tempRoot, "kiro.json"); - WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path"); + WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path"); var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro }; InvokeWriteToConfig(configPath, client); @@ -102,7 +99,7 @@ public void AddsEnvAndDisabledFalse_ForKiro() public void DoesNotAddEnvOrDisabled_ForCursor() { var configPath = Path.Combine(_tempRoot, "cursor.json"); - WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path"); + WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path"); var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor }; InvokeWriteToConfig(configPath, client); @@ -118,7 +115,7 @@ public void DoesNotAddEnvOrDisabled_ForCursor() public void DoesNotAddEnvOrDisabled_ForVSCode() { var configPath = Path.Combine(_tempRoot, "vscode.json"); - WriteInitialConfig(configPath, isVSCode:true, command:_fakeUvPath, directory:"/old/path"); + WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path"); var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode }; InvokeWriteToConfig(configPath, client); @@ -219,25 +216,15 @@ private static void WriteInitialConfig(string configPath, bool isVSCode, string File.WriteAllText(configPath, root.ToString()); } - private static MCPForUnityEditorWindow CreateWindow() - { - return ScriptableObject.CreateInstance(); - } - private static void InvokeWriteToConfig(string configPath, McpClient client) { - var window = CreateWindow(); - var mi = typeof(MCPForUnityEditorWindow).GetMethod("WriteToConfig", BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(mi, "Could not find WriteToConfig via reflection"); - - // pythonDir is unused by WriteToConfig, but pass server src to keep it consistent - var result = (string)mi!.Invoke(window, new object[] { - /* pythonDir */ string.Empty, - /* configPath */ configPath, - /* mcpClient */ client - }); - - Assert.AreEqual("Configured successfully", result, "WriteToConfig should return success"); + var result = McpConfigurationHelper.WriteMcpConfiguration( + pythonDir: string.Empty, + configPath: configPath, + mcpClient: client + ); + + Assert.AreEqual("Configured successfully", result, "WriteMcpConfiguration should return success"); } } } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs.meta similarity index 100% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs.meta rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs.meta diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs index 2bbe4616..aed1c964 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using NUnit.Framework; using MCPForUnity.Editor.Tools; @@ -8,34 +11,41 @@ namespace MCPForUnityTests.Editor.Tools public class CommandRegistryTests { [Test] - public void GetHandler_ThrowException_ForUnknownCommand() + public void GetHandler_ThrowsException_ForUnknownCommand() { - var unknown = "HandleDoesNotExist"; - try - { - var handler = CommandRegistry.GetHandler(unknown); - Assert.Fail("Should throw InvalidOperation for unknown handler."); - } - catch (InvalidOperationException) - { + var unknown = "nonexistent_command_that_should_not_exist"; - } - catch + Assert.Throws(() => { - Assert.Fail("Should throw InvalidOperation for unknown handler."); - } + CommandRegistry.GetHandler(unknown); + }, "Should throw InvalidOperationException for unknown handler"); } [Test] - public void GetHandler_ReturnsManageGameObjectHandler() + public void AutoDiscovery_RegistersAllBuiltInTools() { - var handler = CommandRegistry.GetHandler("manage_gameobject"); - Assert.IsNotNull(handler, "Expected a handler for manage_gameobject."); + // Verify that all expected built-in tools are registered by trying to get their handlers + var expectedTools = new[] + { + "manage_asset", + "manage_editor", + "manage_gameobject", + "manage_scene", + "manage_script", + "manage_shader", + "read_console", + "manage_menu_item", + "manage_prefabs" + }; - var methodInfo = handler.Method; - Assert.AreEqual("HandleCommand", methodInfo.Name, "Handler method name should be HandleCommand."); - Assert.AreEqual(typeof(ManageGameObject), methodInfo.DeclaringType, "Handler should be declared on ManageGameObject."); - Assert.IsNull(handler.Target, "Handler should be a static method (no target instance)."); + foreach (var toolName in expectedTools) + { + Assert.DoesNotThrow(() => + { + var handler = CommandRegistry.GetHandler(toolName); + Assert.IsNotNull(handler, $"Handler for '{toolName}' should not be null"); + }, $"Expected tool '{toolName}' to be auto-registered"); + } } } } diff --git a/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs index 2f7b5ca1..ce6efef2 100644 --- a/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs +++ b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs @@ -80,69 +80,6 @@ public static DependencyCheckResult CheckAllDependencies() return result; } - /// - /// Quick check if system is ready for MCP operations - /// - public static bool IsSystemReady() - { - try - { - var result = CheckAllDependencies(); - return result.IsSystemReady; - } - catch - { - return false; - } - } - - /// - /// Get a summary of missing dependencies - /// - public static string GetMissingDependenciesSummary() - { - try - { - var result = CheckAllDependencies(); - var missing = result.GetMissingRequired(); - - if (missing.Count == 0) - { - return "All required dependencies are available."; - } - - var names = missing.Select(d => d.Name).ToArray(); - return $"Missing required dependencies: {string.Join(", ", names)}"; - } - catch (Exception ex) - { - return $"Error checking dependencies: {ex.Message}"; - } - } - - /// - /// Check if a specific dependency is available - /// - public static bool IsDependencyAvailable(string dependencyName) - { - try - { - var detector = GetCurrentPlatformDetector(); - - return dependencyName.ToLowerInvariant() switch - { - "python" => detector.DetectPython().IsAvailable, - "uv" => detector.DetectUV().IsAvailable, - "mcpserver" or "mcp-server" => detector.DetectMCPServer().IsAvailable, - _ => false - }; - } - catch - { - return false; - } - } - /// /// Get installation recommendations for the current platform /// @@ -175,104 +112,6 @@ public static (string pythonUrl, string uvUrl) GetInstallationUrls() } } - /// - /// Validate that the MCP server can be started - /// - public static bool ValidateMCPServerStartup() - { - try - { - // Check if Python and UV are available - if (!IsDependencyAvailable("python") || !IsDependencyAvailable("uv")) - { - return false; - } - - // Try to ensure server is installed - ServerInstaller.EnsureServerInstalled(); - - // Check if server files exist - var serverStatus = GetCurrentPlatformDetector().DetectMCPServer(); - return serverStatus.IsAvailable; - } - catch (Exception ex) - { - McpLog.Error($"Error validating MCP server startup: {ex.Message}"); - return false; - } - } - - /// - /// Attempt to repair the Python environment - /// - public static bool RepairPythonEnvironment() - { - try - { - McpLog.Info("Attempting to repair Python environment..."); - return ServerInstaller.RepairPythonEnvironment(); - } - catch (Exception ex) - { - McpLog.Error($"Error repairing Python environment: {ex.Message}"); - return false; - } - } - - /// - /// Get detailed dependency information for diagnostics - /// - public static string GetDependencyDiagnostics() - { - try - { - var result = CheckAllDependencies(); - var detector = GetCurrentPlatformDetector(); - - var diagnostics = new System.Text.StringBuilder(); - diagnostics.AppendLine($"Platform: {detector.PlatformName}"); - diagnostics.AppendLine($"Check Time: {result.CheckedAt:yyyy-MM-dd HH:mm:ss} UTC"); - diagnostics.AppendLine($"System Ready: {result.IsSystemReady}"); - diagnostics.AppendLine(); - - foreach (var dep in result.Dependencies) - { - diagnostics.AppendLine($"=== {dep.Name} ==="); - diagnostics.AppendLine($"Available: {dep.IsAvailable}"); - diagnostics.AppendLine($"Required: {dep.IsRequired}"); - - if (!string.IsNullOrEmpty(dep.Version)) - diagnostics.AppendLine($"Version: {dep.Version}"); - - if (!string.IsNullOrEmpty(dep.Path)) - diagnostics.AppendLine($"Path: {dep.Path}"); - - if (!string.IsNullOrEmpty(dep.Details)) - diagnostics.AppendLine($"Details: {dep.Details}"); - - if (!string.IsNullOrEmpty(dep.ErrorMessage)) - diagnostics.AppendLine($"Error: {dep.ErrorMessage}"); - - diagnostics.AppendLine(); - } - - if (result.RecommendedActions.Count > 0) - { - diagnostics.AppendLine("=== Recommended Actions ==="); - foreach (var action in result.RecommendedActions) - { - diagnostics.AppendLine($"- {action}"); - } - } - - return diagnostics.ToString(); - } - catch (Exception ex) - { - return $"Error generating diagnostics: {ex.Message}"; - } - } - private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector) { var missing = result.GetMissingDependencies(); diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs index 09fded14..4ace9756 100644 --- a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs @@ -159,42 +159,6 @@ private bool TryValidatePython(string pythonPath, out string version, out string return false; } - private bool TryValidateUV(string uvPath, out string version) - { - version = null; - - try - { - var psi = new ProcessStartInfo - { - FileName = uvPath, - Arguments = "--version", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - using var process = Process.Start(psi); - if (process == null) return false; - - string output = process.StandardOutput.ReadToEnd().Trim(); - process.WaitForExit(5000); - - if (process.ExitCode == 0 && output.StartsWith("uv ")) - { - version = output.Substring(3); // Remove "uv " prefix - return true; - } - } - catch - { - // Ignore validation errors - } - - return false; - } - private bool TryFindInPath(string executable, out string fullPath) { fullPath = null; @@ -244,10 +208,5 @@ private bool TryFindInPath(string executable, out string fullPath) return false; } - - private bool TryParseVersion(string version, out int major, out int minor) - { - return base.TryParseVersion(version, out major, out minor); - } } } diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs index 715338ce..c89e7cb9 100644 --- a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs @@ -159,42 +159,6 @@ private bool TryValidatePython(string pythonPath, out string version, out string return false; } - private bool TryValidateUV(string uvPath, out string version) - { - version = null; - - try - { - var psi = new ProcessStartInfo - { - FileName = uvPath, - Arguments = "--version", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - using var process = Process.Start(psi); - if (process == null) return false; - - string output = process.StandardOutput.ReadToEnd().Trim(); - process.WaitForExit(5000); - - if (process.ExitCode == 0 && output.StartsWith("uv ")) - { - version = output.Substring(3); // Remove "uv " prefix - return true; - } - } - catch - { - // Ignore validation errors - } - - return false; - } - private bool TryFindInPath(string executable, out string fullPath) { fullPath = null; @@ -244,10 +208,5 @@ private bool TryFindInPath(string executable, out string fullPath) return false; } - - private bool TryParseVersion(string version, out int major, out int minor) - { - return base.TryParseVersion(version, out major, out minor); - } } } diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs index ea57d5ef..bd9c6f03 100644 --- a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs @@ -147,42 +147,6 @@ private bool TryValidatePython(string pythonPath, out string version, out string return false; } - private bool TryValidateUV(string uvPath, out string version) - { - version = null; - - try - { - var psi = new ProcessStartInfo - { - FileName = uvPath, - Arguments = "--version", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - using var process = Process.Start(psi); - if (process == null) return false; - - string output = process.StandardOutput.ReadToEnd().Trim(); - process.WaitForExit(5000); - - if (process.ExitCode == 0 && output.StartsWith("uv ")) - { - version = output.Substring(3); // Remove "uv " prefix - return true; - } - } - catch - { - // Ignore validation errors - } - - return false; - } - private bool TryFindInPath(string executable, out string fullPath) { fullPath = null; @@ -223,10 +187,5 @@ private bool TryFindInPath(string executable, out string fullPath) return false; } - - private bool TryParseVersion(string version, out int major, out int minor) - { - return base.TryParseVersion(version, out major, out minor); - } } } diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index 26c0fbb2..f41e03c3 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -423,105 +423,61 @@ private static void CopyDirectoryRecursive(string sourceDir, string destinationD } } - public static bool RepairPythonEnvironment() + public static bool RebuildMcpServer() { try { - string serverSrc = GetServerPath(); - bool hasServer = File.Exists(Path.Combine(serverSrc, "server.py")); - if (!hasServer) - { - // In dev mode or if not installed yet, try the embedded/dev source - if (TryGetEmbeddedServerSource(out string embeddedSrc) && File.Exists(Path.Combine(embeddedSrc, "server.py"))) - { - serverSrc = embeddedSrc; - hasServer = true; - } - else - { - // Attempt to install then retry - EnsureServerInstalled(); - serverSrc = GetServerPath(); - hasServer = File.Exists(Path.Combine(serverSrc, "server.py")); - } - } - - if (!hasServer) - { - Debug.LogWarning("RepairPythonEnvironment: server.py not found; ensure server is installed first."); - return false; - } - - // Remove stale venv and pinned version file if present - string venvPath = Path.Combine(serverSrc, ".venv"); - if (Directory.Exists(venvPath)) - { - try { Directory.Delete(venvPath, recursive: true); } catch (Exception ex) { Debug.LogWarning($"Failed to delete .venv: {ex.Message}"); } - } - string pyPin = Path.Combine(serverSrc, ".python-version"); - if (File.Exists(pyPin)) - { - try { File.Delete(pyPin); } catch (Exception ex) { Debug.LogWarning($"Failed to delete .python-version: {ex.Message}"); } - } - - string uvPath = FindUvPath(); - if (uvPath == null) + // Find embedded source + if (!TryGetEmbeddedServerSource(out string embeddedSrc)) { - Debug.LogError("UV not found. Please install uv (https://docs.astral.sh/uv/)."); + Debug.LogError("RebuildMcpServer: Could not find embedded server source."); return false; } - var psi = new System.Diagnostics.ProcessStartInfo - { - FileName = uvPath, - Arguments = "sync", - WorkingDirectory = serverSrc, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; + string saveLocation = GetSaveLocation(); + string destRoot = Path.Combine(saveLocation, ServerFolder); + string destSrc = Path.Combine(destRoot, "src"); - using var proc = new System.Diagnostics.Process { StartInfo = psi }; - var sbOut = new StringBuilder(); - var sbErr = new StringBuilder(); - proc.OutputDataReceived += (_, e) => { if (e.Data != null) sbOut.AppendLine(e.Data); }; - proc.ErrorDataReceived += (_, e) => { if (e.Data != null) sbErr.AppendLine(e.Data); }; + // Kill any running uv processes for this server + TryKillUvForPath(destSrc); - if (!proc.Start()) + // Delete the entire installed server directory + if (Directory.Exists(destRoot)) { - Debug.LogError("Failed to start uv process."); - return false; + try + { + Directory.Delete(destRoot, recursive: true); + Debug.Log($"MCP-FOR-UNITY: Deleted existing server at {destRoot}"); + } + catch (Exception ex) + { + Debug.LogError($"Failed to delete existing server: {ex.Message}"); + return false; + } } - proc.BeginOutputReadLine(); - proc.BeginErrorReadLine(); + // Re-copy from embedded source + string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; + Directory.CreateDirectory(destRoot); + CopyDirectoryRecursive(embeddedRoot, destRoot); - if (!proc.WaitForExit(60000)) + // Write version file + string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown"; + try { - try { proc.Kill(); } catch { } - Debug.LogError("uv sync timed out."); - return false; + File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer); } - - // Ensure async buffers flushed - proc.WaitForExit(); - - string stdout = sbOut.ToString(); - string stderr = sbErr.ToString(); - - if (proc.ExitCode != 0) + catch (Exception ex) { - Debug.LogError($"uv sync failed: {stderr}\n{stdout}"); - return false; + Debug.LogWarning($"Failed to write version file: {ex.Message}"); } - Debug.Log("MCP-FOR-UNITY: Python environment repaired successfully."); + Debug.Log($"MCP-FOR-UNITY: Server rebuilt successfully at {destRoot} (version {embeddedVer})"); return true; } catch (Exception ex) { - Debug.LogError($"RepairPythonEnvironment failed: {ex.Message}"); + Debug.LogError($"RebuildMcpServer failed: {ex.Message}"); return false; } } diff --git a/UnityMcpBridge/Editor/MCPForUnityBridge.cs b/UnityMcpBridge/Editor/MCPForUnityBridge.cs index 39312ce4..dcc469b0 100644 --- a/UnityMcpBridge/Editor/MCPForUnityBridge.cs +++ b/UnityMcpBridge/Editor/MCPForUnityBridge.cs @@ -387,6 +387,7 @@ public static void Start() // Start background listener with cooperative cancellation cts = new CancellationTokenSource(); listenerTask = Task.Run(() => ListenerLoopAsync(cts.Token)); + CommandRegistry.Initialize(); EditorApplication.update += ProcessCommands; // Ensure lifecycle events are (re)subscribed in case Stop() removed them earlier in-domain try { AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; } catch { } diff --git a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs index 2503391d..79003d55 100644 --- a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs +++ b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs @@ -1,50 +1,138 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; -using MCPForUnity.Editor.Tools.MenuItems; -using MCPForUnity.Editor.Tools.Prefabs; namespace MCPForUnity.Editor.Tools { /// - /// Registry for all MCP command handlers (Refactored Version) + /// Registry for all MCP command handlers via reflection. /// public static class CommandRegistry { - // Maps command names (matching those called from Python via ctx.bridge.unity_editor.HandlerName) - // to the corresponding static HandleCommand method in the appropriate tool class. - private static readonly Dictionary> _handlers = new() + private static readonly Dictionary> _handlers = new(); + private static bool _initialized = false; + + /// + /// Initialize and auto-discover all tools marked with [McpForUnityTool] + /// + public static void Initialize() { - { "manage_script", ManageScript.HandleCommand }, - { "manage_scene", ManageScene.HandleCommand }, - { "manage_editor", ManageEditor.HandleCommand }, - { "manage_gameobject", ManageGameObject.HandleCommand }, - { "manage_asset", ManageAsset.HandleCommand }, - { "read_console", ReadConsole.HandleCommand }, - { "manage_menu_item", ManageMenuItem.HandleCommand }, - { "manage_shader", ManageShader.HandleCommand}, - { "manage_prefabs", ManagePrefabs.HandleCommand}, - }; + if (_initialized) return; + + AutoDiscoverTools(); + _initialized = true; + } /// - /// Gets a command handler by name. + /// Convert PascalCase or camelCase to snake_case /// - /// Name of the command handler (e.g., "HandleManageAsset"). - /// The command handler function if found, null otherwise. - public static Func GetHandler(string commandName) + private static string ToSnakeCase(string name) { - if (!_handlers.TryGetValue(commandName, out var handler)) + if (string.IsNullOrEmpty(name)) return name; + + // Insert underscore before uppercase letters (except first) + var s1 = Regex.Replace(name, "(.)([A-Z][a-z]+)", "$1_$2"); + var s2 = Regex.Replace(s1, "([a-z0-9])([A-Z])", "$1_$2"); + return s2.ToLower(); + } + + /// + /// Auto-discover all types with [McpForUnityTool] attribute + /// + private static void AutoDiscoverTools() + { + try { - throw new InvalidOperationException( - $"Unknown or unsupported command type: {commandName}"); + var toolTypes = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) + .SelectMany(a => + { + try { return a.GetTypes(); } + catch { return new Type[0]; } + }) + .Where(t => t.GetCustomAttribute() != null); + + foreach (var type in toolTypes) + { + RegisterToolType(type); + } + + McpLog.Info($"Auto-discovered {_handlers.Count} tools"); } + catch (Exception ex) + { + McpLog.Error($"Failed to auto-discover MCP tools: {ex.Message}"); + } + } - return handler; + private static void RegisterToolType(Type type) + { + var attr = type.GetCustomAttribute(); + + // Get command name (explicit or auto-generated) + string commandName = attr.CommandName; + if (string.IsNullOrEmpty(commandName)) + { + commandName = ToSnakeCase(type.Name); + } + + // Check for duplicate command names + if (_handlers.ContainsKey(commandName)) + { + McpLog.Warn( + $"Duplicate command name '{commandName}' detected. " + + $"Tool {type.Name} will override previously registered handler." + ); + } + + // Find HandleCommand method + var method = type.GetMethod( + "HandleCommand", + BindingFlags.Public | BindingFlags.Static, + null, + new[] { typeof(JObject) }, + null + ); + + if (method == null) + { + McpLog.Warn( + $"MCP tool {type.Name} is marked with [McpForUnityTool] " + + $"but has no public static HandleCommand(JObject) method" + ); + return; + } + + try + { + var handler = (Func)Delegate.CreateDelegate( + typeof(Func), + method + ); + _handlers[commandName] = handler; + } + catch (Exception ex) + { + McpLog.Error($"Failed to register tool {type.Name}: {ex.Message}"); + } } - public static void Add(string commandName, Func handler) + /// + /// Get a command handler by name + /// + public static Func GetHandler(string commandName) { - _handlers.Add(commandName, handler); + if (!_handlers.TryGetValue(commandName, out var handler)) + { + throw new InvalidOperationException( + $"Unknown or unsupported command type: {commandName}" + ); + } + return handler; } } } diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index 52a5bcac..1a952f37 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -22,6 +22,7 @@ namespace MCPForUnity.Editor.Tools /// /// Handles asset management operations within the Unity project. /// + [McpForUnityTool("manage_asset")] public static class ManageAsset { // --- Main Handler --- diff --git a/UnityMcpBridge/Editor/Tools/ManageEditor.cs b/UnityMcpBridge/Editor/Tools/ManageEditor.cs index f26502dd..f8255224 100644 --- a/UnityMcpBridge/Editor/Tools/ManageEditor.cs +++ b/UnityMcpBridge/Editor/Tools/ManageEditor.cs @@ -15,6 +15,7 @@ namespace MCPForUnity.Editor.Tools /// Handles operations related to controlling and querying the Unity Editor state, /// including managing Tags and Layers. /// + [McpForUnityTool("manage_editor")] public static class ManageEditor { // Constant for starting user layer index diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 71a379b0..40504a87 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -19,6 +19,7 @@ namespace MCPForUnity.Editor.Tools /// /// Handles GameObject manipulation within the current scene (CRUD, find, components). /// + [McpForUnityTool("manage_gameobject")] public static class ManageGameObject { // Shared JsonSerializer to avoid per-call allocation overhead diff --git a/UnityMcpBridge/Editor/Tools/ManageScene.cs b/UnityMcpBridge/Editor/Tools/ManageScene.cs index beab65b2..6a310d02 100644 --- a/UnityMcpBridge/Editor/Tools/ManageScene.cs +++ b/UnityMcpBridge/Editor/Tools/ManageScene.cs @@ -14,6 +14,7 @@ namespace MCPForUnity.Editor.Tools /// /// Handles scene management operations like loading, saving, creating, and querying hierarchy. /// + [McpForUnityTool("manage_scene")] public static class ManageScene { private sealed class SceneCommand diff --git a/UnityMcpBridge/Editor/Tools/ManageScript.cs b/UnityMcpBridge/Editor/Tools/ManageScript.cs index 073b4b98..2d970486 100644 --- a/UnityMcpBridge/Editor/Tools/ManageScript.cs +++ b/UnityMcpBridge/Editor/Tools/ManageScript.cs @@ -49,6 +49,7 @@ namespace MCPForUnity.Editor.Tools /// Note: Without Roslyn, the system falls back to basic structural validation. /// Roslyn provides full C# compiler diagnostics with line numbers and detailed error messages. /// + [McpForUnityTool("manage_script")] public static class ManageScript { /// diff --git a/UnityMcpBridge/Editor/Tools/ManageShader.cs b/UnityMcpBridge/Editor/Tools/ManageShader.cs index bcd1a6aa..2d7f4d0a 100644 --- a/UnityMcpBridge/Editor/Tools/ManageShader.cs +++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs @@ -12,6 +12,7 @@ namespace MCPForUnity.Editor.Tools /// /// Handles CRUD operations for shader files within the Unity project. /// + [McpForUnityTool("manage_shader")] public static class ManageShader { /// diff --git a/UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs b/UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs new file mode 100644 index 00000000..bb4e0431 --- /dev/null +++ b/UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs @@ -0,0 +1,37 @@ +using System; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Marks a class as an MCP tool handler for auto-discovery. + /// The class must have a public static HandleCommand(JObject) method. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class McpForUnityToolAttribute : Attribute + { + /// + /// The command name used to route requests to this tool. + /// If not specified, defaults to the PascalCase class name converted to snake_case. + /// + public string CommandName { get; } + + /// + /// Create an MCP tool attribute with auto-generated command name. + /// The command name will be derived from the class name (PascalCase → snake_case). + /// Example: ManageAsset → manage_asset + /// + public McpForUnityToolAttribute() + { + CommandName = null; // Will be auto-generated + } + + /// + /// Create an MCP tool attribute with explicit command name. + /// + /// The command name (e.g., "manage_asset") + public McpForUnityToolAttribute(string commandName) + { + CommandName = commandName; + } + } +} diff --git a/UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs.meta b/UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs.meta new file mode 100644 index 00000000..57242c17 --- /dev/null +++ b/UnityMcpBridge/Editor/Tools/McpForUnityToolAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 804d07b886f4e4eb39316bbef34687c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs b/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs index 0f213c68..e4b7eaf7 100644 --- a/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs +++ b/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs @@ -4,6 +4,7 @@ namespace MCPForUnity.Editor.Tools.MenuItems { + [McpForUnityTool("manage_menu_item")] public static class ManageMenuItem { /// diff --git a/UnityMcpBridge/Editor/Tools/Prefabs/ManagePrefabs.cs b/UnityMcpBridge/Editor/Tools/Prefabs/ManagePrefabs.cs index aaf67b14..9e68d20e 100644 --- a/UnityMcpBridge/Editor/Tools/Prefabs/ManagePrefabs.cs +++ b/UnityMcpBridge/Editor/Tools/Prefabs/ManagePrefabs.cs @@ -9,6 +9,7 @@ namespace MCPForUnity.Editor.Tools.Prefabs { + [McpForUnityTool("manage_prefabs")] public static class ManagePrefabs { private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject"; diff --git a/UnityMcpBridge/Editor/Tools/ReadConsole.cs b/UnityMcpBridge/Editor/Tools/ReadConsole.cs index e58f4e79..8a0147e3 100644 --- a/UnityMcpBridge/Editor/Tools/ReadConsole.cs +++ b/UnityMcpBridge/Editor/Tools/ReadConsole.cs @@ -14,6 +14,7 @@ namespace MCPForUnity.Editor.Tools /// Handles reading and clearing Unity Editor console log entries. /// Uses reflection to access internal LogEntry methods/properties. /// + [McpForUnityTool("read_console")] public static class ReadConsole { // (Calibration removed) diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index ed70181b..98a5295e 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -368,25 +368,25 @@ private void DrawServerStatusSection() } EditorGUILayout.Space(4); - // Repair Python Env button with tooltip tag + // Rebuild MCP Server button with tooltip tag using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); GUIContent repairLabel = new GUIContent( - "Repair Python Env", - "Deletes the server's .venv and runs 'uv sync' to rebuild a clean environment. Use this if modules are missing or Python upgraded." + "Rebuild MCP Server", + "Deletes the installed server and re-copies it from the package. Use this to update the server after making source code changes or if the installation is corrupted." ); if (GUILayout.Button(repairLabel, GUILayout.Width(160), GUILayout.Height(22))) { - bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RepairPythonEnvironment(); + bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RebuildMcpServer(); if (ok) { - EditorUtility.DisplayDialog("MCP For Unity", "Python environment repaired.", "OK"); + EditorUtility.DisplayDialog("MCP For Unity", "Server rebuilt successfully.", "OK"); UpdatePythonServerInstallationStatus(); } else { - EditorUtility.DisplayDialog("MCP For Unity", "Repair failed. Please check Console for details.", "OK"); + EditorUtility.DisplayDialog("MCP For Unity", "Rebuild failed. Please check Console for details.", "OK"); } } } diff --git a/UnityMcpBridge/README.md b/UnityMcpBridge/README.md index b073a5fc..b26b9f19 100644 --- a/UnityMcpBridge/README.md +++ b/UnityMcpBridge/README.md @@ -29,7 +29,7 @@ The window has four areas: Server Status, Unity Bridge, MCP Client Configuration - Ports: Unity (varies; shown in UI), MCP 6500. - Actions: - Auto-Setup: Registers/updates your selected MCP client(s), ensures bridge connectivity. Shows “Connected ✓” after success. - - Repair Python Env: Rebuilds a clean Python environment (deletes `.venv`, runs `uv sync`). + - Rebuild MCP Server: Rebuilds the Python based MCP server - Select server folder…: Choose the folder containing `server.py`. - Verify again: Re-checks server presence. - If Python isn’t detected, use “Open Install Instructions”. diff --git a/UnityMcpBridge/UnityMcpServer~/src/registry/__init__.py b/UnityMcpBridge/UnityMcpServer~/src/registry/__init__.py new file mode 100644 index 00000000..5beb708b --- /dev/null +++ b/UnityMcpBridge/UnityMcpServer~/src/registry/__init__.py @@ -0,0 +1,14 @@ +""" +Registry package for MCP tool auto-discovery. +""" +from .tool_registry import ( + mcp_for_unity_tool, + get_registered_tools, + clear_registry +) + +__all__ = [ + 'mcp_for_unity_tool', + 'get_registered_tools', + 'clear_registry' +] diff --git a/UnityMcpBridge/UnityMcpServer~/src/registry/tool_registry.py b/UnityMcpBridge/UnityMcpServer~/src/registry/tool_registry.py new file mode 100644 index 00000000..bbe36439 --- /dev/null +++ b/UnityMcpBridge/UnityMcpServer~/src/registry/tool_registry.py @@ -0,0 +1,51 @@ +""" +Tool registry for auto-discovery of MCP tools. +""" +from typing import Callable, Any + +# Global registry to collect decorated tools +_tool_registry: list[dict[str, Any]] = [] + + +def mcp_for_unity_tool( + name: str | None = None, + description: str | None = None, + **kwargs +) -> Callable: + """ + Decorator for registering MCP tools in the server's tools directory. + + Tools are registered in the global tool registry. + + Args: + name: Tool name (defaults to function name) + description: Tool description + **kwargs: Additional arguments passed to @mcp.tool() + + Example: + @mcp_for_unity_tool(description="Does something cool") + async def my_custom_tool(ctx: Context, ...): + pass + """ + def decorator(func: Callable) -> Callable: + tool_name = name if name is not None else func.__name__ + _tool_registry.append({ + 'func': func, + 'name': tool_name, + 'description': description, + 'kwargs': kwargs + }) + + return func + + return decorator + + +def get_registered_tools() -> list[dict[str, Any]]: + """Get all registered tools""" + return _tool_registry.copy() + + +def clear_registry(): + """Clear the tool registry (useful for testing)""" + _tool_registry.clear() diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/__init__.py b/UnityMcpBridge/UnityMcpServer~/src/tools/__init__.py index 5bf45f2e..6ede53d3 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/__init__.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/__init__.py @@ -1,35 +1,60 @@ +""" +MCP Tools package - Auto-discovers and registers all tools in this directory. +""" +import importlib import logging +from pathlib import Path +import pkgutil from mcp.server.fastmcp import FastMCP +from telemetry_decorator import telemetry_tool -from .manage_script_edits import register_manage_script_edits_tools -from .manage_script import register_manage_script_tools -from .manage_scene import register_manage_scene_tools -from .manage_editor import register_manage_editor_tools -from .manage_gameobject import register_manage_gameobject_tools -from .manage_asset import register_manage_asset_tools -from .manage_prefabs import register_manage_prefabs_tools -from .manage_shader import register_manage_shader_tools -from .read_console import register_read_console_tools -from .manage_menu_item import register_manage_menu_item_tools -from .resource_tools import register_resource_tools +from registry import get_registered_tools, mcp_for_unity_tool logger = logging.getLogger("mcp-for-unity-server") +# Export decorator for easy imports within tools +__all__ = ['register_all_tools', 'mcp_for_unity_tool'] + def register_all_tools(mcp: FastMCP): - """Register all refactored tools with the MCP server.""" - # Prefer the surgical edits tool so LLMs discover it first - logger.info("Registering MCP for Unity Server refactored tools...") - register_manage_script_edits_tools(mcp) - register_manage_script_tools(mcp) - register_manage_scene_tools(mcp) - register_manage_editor_tools(mcp) - register_manage_gameobject_tools(mcp) - register_manage_asset_tools(mcp) - register_manage_prefabs_tools(mcp) - register_manage_shader_tools(mcp) - register_read_console_tools(mcp) - register_manage_menu_item_tools(mcp) - register_resource_tools(mcp) - logger.info("MCP for Unity Server tool registration complete.") + """ + Auto-discover and register all tools in the tools/ directory. + + Any .py file in this directory with @mcp_for_unity_tool decorated + functions will be automatically registered. + """ + logger.info("Auto-discovering MCP for Unity Server tools...") + # Dynamic import of all modules in this directory + tools_dir = Path(__file__).parent + + for _, module_name, _ in pkgutil.iter_modules([str(tools_dir)]): + # Skip private modules and __init__ + if module_name.startswith('_'): + continue + + try: + importlib.import_module(f'.{module_name}', __package__) + except Exception as e: + logger.warning(f"Failed to import tool module {module_name}: {e}") + + tools = get_registered_tools() + + if not tools: + logger.warning("No MCP tools registered!") + return + + for tool_info in tools: + func = tool_info['func'] + tool_name = tool_info['name'] + description = tool_info['description'] + kwargs = tool_info['kwargs'] + + # Apply the @mcp.tool decorator and telemetry + wrapped = mcp.tool( + name=tool_name, description=description, **kwargs)(func) + wrapped = telemetry_tool(tool_name)(wrapped) + tool_info['func'] = wrapped + logger.info(f"Registered tool: {tool_name} - {description}") + + logger.info(f"Registered {len(tools)} MCP tools") diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_asset.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_asset.py index a442b422..5e21d2ce 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_asset.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_asset.py @@ -4,83 +4,80 @@ import asyncio from typing import Annotated, Any, Literal -from mcp.server.fastmcp import FastMCP, Context - +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from unity_connection import async_send_command_with_retry -from telemetry_decorator import telemetry_tool - -def register_manage_asset_tools(mcp: FastMCP): - """Registers the manage_asset tool with the MCP server.""" - @mcp.tool(name="manage_asset", description="Performs asset operations (import, create, modify, delete, etc.) in Unity.") - @telemetry_tool("manage_asset") - async def manage_asset( - ctx: Context, - action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."], - path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."], - asset_type: Annotated[str, - "Asset type (e.g., 'Material', 'Folder') - required for 'create'."] | None = None, - properties: Annotated[dict[str, Any], - "Dictionary of properties for 'create'/'modify'."] | None = None, - destination: Annotated[str, - "Target path for 'duplicate'/'move'."] | None = None, - generate_preview: Annotated[bool, - "Generate a preview/thumbnail for the asset when supported."] = False, - search_pattern: Annotated[str, - "Search pattern (e.g., '*.prefab')."] | None = None, - filter_type: Annotated[str, "Filter type for search"] | None = None, - filter_date_after: Annotated[str, - "Date after which to filter"] | None = None, - page_size: Annotated[int, "Page size for pagination"] | None = None, - page_number: Annotated[int, "Page number for pagination"] | None = None - ) -> dict[str, Any]: - ctx.info(f"Processing manage_asset: {action}") - # Ensure properties is a dict if None - if properties is None: - properties = {} +@mcp_for_unity_tool( + description="Performs asset operations (import, create, modify, delete, etc.) in Unity." +) +async def manage_asset( + ctx: Context, + action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."], + path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."], + asset_type: Annotated[str, + "Asset type (e.g., 'Material', 'Folder') - required for 'create'."] | None = None, + properties: Annotated[dict[str, Any], + "Dictionary of properties for 'create'/'modify'."] | None = None, + destination: Annotated[str, + "Target path for 'duplicate'/'move'."] | None = None, + generate_preview: Annotated[bool, + "Generate a preview/thumbnail for the asset when supported."] = False, + search_pattern: Annotated[str, + "Search pattern (e.g., '*.prefab')."] | None = None, + filter_type: Annotated[str, "Filter type for search"] | None = None, + filter_date_after: Annotated[str, + "Date after which to filter"] | None = None, + page_size: Annotated[int, "Page size for pagination"] | None = None, + page_number: Annotated[int, "Page number for pagination"] | None = None +) -> dict[str, Any]: + ctx.info(f"Processing manage_asset: {action}") + # Ensure properties is a dict if None + if properties is None: + properties = {} - # Coerce numeric inputs defensively - def _coerce_int(value, default=None): - if value is None: + # Coerce numeric inputs defensively + def _coerce_int(value, default=None): + if value is None: + return default + try: + if isinstance(value, bool): return default - try: - if isinstance(value, bool): - return default - if isinstance(value, int): - return int(value) - s = str(value).strip() - if s.lower() in ("", "none", "null"): - return default - return int(float(s)) - except Exception: + if isinstance(value, int): + return int(value) + s = str(value).strip() + if s.lower() in ("", "none", "null"): return default + return int(float(s)) + except Exception: + return default - page_size = _coerce_int(page_size) - page_number = _coerce_int(page_number) + page_size = _coerce_int(page_size) + page_number = _coerce_int(page_number) - # Prepare parameters for the C# handler - params_dict = { - "action": action.lower(), - "path": path, - "assetType": asset_type, - "properties": properties, - "destination": destination, - "generatePreview": generate_preview, - "searchPattern": search_pattern, - "filterType": filter_type, - "filterDateAfter": filter_date_after, - "pageSize": page_size, - "pageNumber": page_number - } + # Prepare parameters for the C# handler + params_dict = { + "action": action.lower(), + "path": path, + "assetType": asset_type, + "properties": properties, + "destination": destination, + "generatePreview": generate_preview, + "searchPattern": search_pattern, + "filterType": filter_type, + "filterDateAfter": filter_date_after, + "pageSize": page_size, + "pageNumber": page_number + } - # Remove None values to avoid sending unnecessary nulls - params_dict = {k: v for k, v in params_dict.items() if v is not None} + # Remove None values to avoid sending unnecessary nulls + params_dict = {k: v for k, v in params_dict.items() if v is not None} - # Get the current asyncio event loop - loop = asyncio.get_running_loop() + # Get the current asyncio event loop + loop = asyncio.get_running_loop() - # Use centralized async retry helper to avoid blocking the event loop - result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop) - # Return the result obtained from Unity - return result if isinstance(result, dict) else {"success": False, "message": str(result)} + # Use centralized async retry helper to avoid blocking the event loop + result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop) + # Return the result obtained from Unity + return result if isinstance(result, dict) else {"success": False, "message": str(result)} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_editor.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_editor.py index 644209f7..c0de76c2 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_editor.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_editor.py @@ -1,60 +1,57 @@ from typing import Annotated, Any, Literal -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from telemetry import is_telemetry_enabled, record_tool_usage - from unity_connection import send_command_with_retry -def register_manage_editor_tools(mcp: FastMCP): - """Register all editor management tools with the MCP server.""" - - @mcp.tool(name="manage_editor", description="Controls and queries the Unity editor's state and settings") - @telemetry_tool("manage_editor") - def manage_editor( - ctx: Context, - action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "get_state", "get_project_root", "get_windows", - "get_active_tool", "get_selection", "get_prefab_stage", "set_active_tool", "add_tag", "remove_tag", "get_tags", "add_layer", "remove_layer", "get_layers"], "Get and update the Unity Editor state."], - wait_for_completion: Annotated[bool, - "Optional. If True, waits for certain actions"] | None = None, - tool_name: Annotated[str, - "Tool name when setting active tool"] | None = None, - tag_name: Annotated[str, - "Tag name when adding and removing tags"] | None = None, - layer_name: Annotated[str, - "Layer name when adding and removing layers"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing manage_editor: {action}") - try: - # Diagnostics: quick telemetry checks - if action == "telemetry_status": - return {"success": True, "telemetry_enabled": is_telemetry_enabled()} - - if action == "telemetry_ping": - record_tool_usage("diagnostic_ping", True, 1.0, None) - return {"success": True, "message": "telemetry ping queued"} - # Prepare parameters, removing None values - params = { - "action": action, - "waitForCompletion": wait_for_completion, - "toolName": tool_name, # Corrected parameter name to match C# - "tagName": tag_name, # Pass tag name - "layerName": layer_name, # Pass layer name - # Add other parameters based on the action being performed - # "width": width, - # "height": height, - # etc. - } - params = {k: v for k, v in params.items() if v is not None} - - # Send command using centralized retry helper - response = send_command_with_retry("manage_editor", params) - - # Preserve structured failure data; unwrap success into a friendlier shape - if isinstance(response, dict) and response.get("success"): - return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")} - return response if isinstance(response, dict) else {"success": False, "message": str(response)} - - except Exception as e: - return {"success": False, "message": f"Python error managing editor: {str(e)}"} +@mcp_for_unity_tool( + description="Controls and queries the Unity editor's state and settings" +) +def manage_editor( + ctx: Context, + action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "get_state", "get_project_root", "get_windows", + "get_active_tool", "get_selection", "get_prefab_stage", "set_active_tool", "add_tag", "remove_tag", "get_tags", "add_layer", "remove_layer", "get_layers"], "Get and update the Unity Editor state."], + wait_for_completion: Annotated[bool, + "Optional. If True, waits for certain actions"] | None = None, + tool_name: Annotated[str, + "Tool name when setting active tool"] | None = None, + tag_name: Annotated[str, + "Tag name when adding and removing tags"] | None = None, + layer_name: Annotated[str, + "Layer name when adding and removing layers"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_editor: {action}") + try: + # Diagnostics: quick telemetry checks + if action == "telemetry_status": + return {"success": True, "telemetry_enabled": is_telemetry_enabled()} + + if action == "telemetry_ping": + record_tool_usage("diagnostic_ping", True, 1.0, None) + return {"success": True, "message": "telemetry ping queued"} + # Prepare parameters, removing None values + params = { + "action": action, + "waitForCompletion": wait_for_completion, + "toolName": tool_name, # Corrected parameter name to match C# + "tagName": tag_name, # Pass tag name + "layerName": layer_name, # Pass layer name + # Add other parameters based on the action being performed + # "width": width, + # "height": height, + # etc. + } + params = {k: v for k, v in params.items() if v is not None} + + # Send command using centralized retry helper + response = send_command_with_retry("manage_editor", params) + + # Preserve structured failure data; unwrap success into a friendlier shape + if isinstance(response, dict) and response.get("success"): + return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")} + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + + except Exception as e: + return {"success": False, "message": f"Python error managing editor: {str(e)}"} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py index 41d4a1c0..a8ca1609 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py @@ -1,148 +1,145 @@ from typing import Annotated, Any, Literal -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool - +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry -def register_manage_gameobject_tools(mcp: FastMCP): - """Register all GameObject management tools with the MCP server.""" - - @mcp.tool(name="manage_gameobject", description="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data.") - @telemetry_tool("manage_gameobject") - def manage_gameobject( - ctx: Context, - action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."], - target: Annotated[str, - "GameObject identifier by name or path for modify/delete/component actions"] | None = None, - search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"], - "How to find objects. Used with 'find' and some 'target' lookups."] | None = None, - name: Annotated[str, - "GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead."] | None = None, - tag: Annotated[str, - "Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None, - parent: Annotated[str, - "Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None, - position: Annotated[list[float], - "Position - used for both 'create' (initial position) and 'modify' (change position)"] | None = None, - rotation: Annotated[list[float], - "Rotation - used for both 'create' (initial rotation) and 'modify' (change rotation)"] | None = None, - scale: Annotated[list[float], - "Scale - used for both 'create' (initial scale) and 'modify' (change scale)"] | None = None, - components_to_add: Annotated[list[str], - "List of component names to add"] | None = None, - primitive_type: Annotated[str, - "Primitive type for 'create' action"] | None = None, - save_as_prefab: Annotated[bool, - "If True, saves the created GameObject as a prefab"] | None = None, - prefab_path: Annotated[str, "Path for prefab creation"] | None = None, - prefab_folder: Annotated[str, - "Folder for prefab creation"] | None = None, - # --- Parameters for 'modify' --- - set_active: Annotated[bool, - "If True, sets the GameObject active"] | None = None, - layer: Annotated[str, "Layer name"] | None = None, - components_to_remove: Annotated[list[str], - "List of component names to remove"] | None = None, - component_properties: Annotated[dict[str, dict[str, Any]], - """Dictionary of component names to their properties to set. For example: - `{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject - `{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component - Example set nested property: - - Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`"""] | None = None, - # --- Parameters for 'find' --- - search_term: Annotated[str, - "Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None, - find_all: Annotated[bool, - "If True, finds all GameObjects matching the search term"] | None = None, - search_in_children: Annotated[bool, - "If True, searches in children of the GameObject"] | None = None, - search_inactive: Annotated[bool, - "If True, searches inactive GameObjects"] | None = None, - # -- Component Management Arguments -- - component_name: Annotated[str, - "Component name for 'add_component' and 'remove_component' actions"] | None = None, - # Controls whether serialization of private [SerializeField] fields is included - includeNonPublicSerialized: Annotated[bool, - "Controls whether serialization of private [SerializeField] fields is included"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing manage_gameobject: {action}") - try: - # Validate parameter usage to prevent silent failures - if action == "find": - if name is not None: - return { - "success": False, - "message": "For 'find' action, use 'search_term' parameter, not 'name'. Remove 'name' parameter. Example: search_term='Player', search_method='by_name'" - } - if search_term is None: - return { - "success": False, - "message": "For 'find' action, 'search_term' parameter is required. Use search_term (not 'name') to specify what to find." - } +@mcp_for_unity_tool( + description="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data." +) +def manage_gameobject( + ctx: Context, + action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."], + target: Annotated[str, + "GameObject identifier by name or path for modify/delete/component actions"] | None = None, + search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"], + "How to find objects. Used with 'find' and some 'target' lookups."] | None = None, + name: Annotated[str, + "GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead."] | None = None, + tag: Annotated[str, + "Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None, + parent: Annotated[str, + "Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None, + position: Annotated[list[float], + "Position - used for both 'create' (initial position) and 'modify' (change position)"] | None = None, + rotation: Annotated[list[float], + "Rotation - used for both 'create' (initial rotation) and 'modify' (change rotation)"] | None = None, + scale: Annotated[list[float], + "Scale - used for both 'create' (initial scale) and 'modify' (change scale)"] | None = None, + components_to_add: Annotated[list[str], + "List of component names to add"] | None = None, + primitive_type: Annotated[str, + "Primitive type for 'create' action"] | None = None, + save_as_prefab: Annotated[bool, + "If True, saves the created GameObject as a prefab"] | None = None, + prefab_path: Annotated[str, "Path for prefab creation"] | None = None, + prefab_folder: Annotated[str, + "Folder for prefab creation"] | None = None, + # --- Parameters for 'modify' --- + set_active: Annotated[bool, + "If True, sets the GameObject active"] | None = None, + layer: Annotated[str, "Layer name"] | None = None, + components_to_remove: Annotated[list[str], + "List of component names to remove"] | None = None, + component_properties: Annotated[dict[str, dict[str, Any]], + """Dictionary of component names to their properties to set. For example: + `{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject + `{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component + Example set nested property: + - Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`"""] | None = None, + # --- Parameters for 'find' --- + search_term: Annotated[str, + "Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None, + find_all: Annotated[bool, + "If True, finds all GameObjects matching the search term"] | None = None, + search_in_children: Annotated[bool, + "If True, searches in children of the GameObject"] | None = None, + search_inactive: Annotated[bool, + "If True, searches inactive GameObjects"] | None = None, + # -- Component Management Arguments -- + component_name: Annotated[str, + "Component name for 'add_component' and 'remove_component' actions"] | None = None, + # Controls whether serialization of private [SerializeField] fields is included + includeNonPublicSerialized: Annotated[bool, + "Controls whether serialization of private [SerializeField] fields is included"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_gameobject: {action}") + try: + # Validate parameter usage to prevent silent failures + if action == "find": + if name is not None: + return { + "success": False, + "message": "For 'find' action, use 'search_term' parameter, not 'name'. Remove 'name' parameter. Example: search_term='Player', search_method='by_name'" + } + if search_term is None: + return { + "success": False, + "message": "For 'find' action, 'search_term' parameter is required. Use search_term (not 'name') to specify what to find." + } - if action in ["create", "modify"]: - if search_term is not None: - return { - "success": False, - "message": f"For '{action}' action, use 'name' parameter, not 'search_term'." - } + if action in ["create", "modify"]: + if search_term is not None: + return { + "success": False, + "message": f"For '{action}' action, use 'name' parameter, not 'search_term'." + } - # Prepare parameters, removing None values - params = { - "action": action, - "target": target, - "searchMethod": search_method, - "name": name, - "tag": tag, - "parent": parent, - "position": position, - "rotation": rotation, - "scale": scale, - "componentsToAdd": components_to_add, - "primitiveType": primitive_type, - "saveAsPrefab": save_as_prefab, - "prefabPath": prefab_path, - "prefabFolder": prefab_folder, - "setActive": set_active, - "layer": layer, - "componentsToRemove": components_to_remove, - "componentProperties": component_properties, - "searchTerm": search_term, - "findAll": find_all, - "searchInChildren": search_in_children, - "searchInactive": search_inactive, - "componentName": component_name, - "includeNonPublicSerialized": includeNonPublicSerialized - } - params = {k: v for k, v in params.items() if v is not None} + # Prepare parameters, removing None values + params = { + "action": action, + "target": target, + "searchMethod": search_method, + "name": name, + "tag": tag, + "parent": parent, + "position": position, + "rotation": rotation, + "scale": scale, + "componentsToAdd": components_to_add, + "primitiveType": primitive_type, + "saveAsPrefab": save_as_prefab, + "prefabPath": prefab_path, + "prefabFolder": prefab_folder, + "setActive": set_active, + "layer": layer, + "componentsToRemove": components_to_remove, + "componentProperties": component_properties, + "searchTerm": search_term, + "findAll": find_all, + "searchInChildren": search_in_children, + "searchInactive": search_inactive, + "componentName": component_name, + "includeNonPublicSerialized": includeNonPublicSerialized + } + params = {k: v for k, v in params.items() if v is not None} - # --- Handle Prefab Path Logic --- - # Check if 'saveAsPrefab' is explicitly True in params - if action == "create" and params.get("saveAsPrefab"): - if "prefabPath" not in params: - if "name" not in params or not params["name"]: - return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."} - # Use the provided prefab_folder (which has a default) and the name to construct the path - constructed_path = f"{prefab_folder}/{params['name']}.prefab" - # Ensure clean path separators (Unity prefers '/') - params["prefabPath"] = constructed_path.replace("\\", "/") - elif not params["prefabPath"].lower().endswith(".prefab"): - return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"} - # Ensure prefabFolder itself isn't sent if prefabPath was constructed or provided - # The C# side only needs the final prefabPath - params.pop("prefabFolder", None) - # -------------------------------- + # --- Handle Prefab Path Logic --- + # Check if 'saveAsPrefab' is explicitly True in params + if action == "create" and params.get("saveAsPrefab"): + if "prefabPath" not in params: + if "name" not in params or not params["name"]: + return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."} + # Use the provided prefab_folder (which has a default) and the name to construct the path + constructed_path = f"{prefab_folder}/{params['name']}.prefab" + # Ensure clean path separators (Unity prefers '/') + params["prefabPath"] = constructed_path.replace("\\", "/") + elif not params["prefabPath"].lower().endswith(".prefab"): + return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"} + # Ensure prefabFolder itself isn't sent if prefabPath was constructed or provided + # The C# side only needs the final prefabPath + params.pop("prefabFolder", None) + # -------------------------------- - # Use centralized retry helper - response = send_command_with_retry("manage_gameobject", params) + # Use centralized retry helper + response = send_command_with_retry("manage_gameobject", params) - # Check if the response indicates success - # If the response is not successful, raise an exception with the error message - if isinstance(response, dict) and response.get("success"): - return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")} - return response if isinstance(response, dict) else {"success": False, "message": str(response)} + # Check if the response indicates success + # If the response is not successful, raise an exception with the error message + if isinstance(response, dict) and response.get("success"): + return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")} + return response if isinstance(response, dict) else {"success": False, "message": str(response)} - except Exception as e: - return {"success": False, "message": f"Python error managing GameObject: {str(e)}"} \ No newline at end of file + except Exception as e: + return {"success": False, "message": f"Python error managing GameObject: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_menu_item.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_menu_item.py index 3e7620a6..5463614d 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_menu_item.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_menu_item.py @@ -4,41 +4,38 @@ import asyncio from typing import Annotated, Any, Literal -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool - +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from unity_connection import async_send_command_with_retry -def register_manage_menu_item_tools(mcp: FastMCP): - """Registers the manage_menu_item tool with the MCP server.""" - - @mcp.tool(name="manage_menu_item", description="Manage Unity menu items (execute/list/exists). If you're not sure what menu item to use, use the 'list' action to find it before using 'execute'.") - @telemetry_tool("manage_menu_item") - async def manage_menu_item( - ctx: Context, - action: Annotated[Literal["execute", "list", "exists"], "Read and execute Unity menu items."], - menu_path: Annotated[str, - "Menu path for 'execute' or 'exists' (e.g., 'File/Save Project')"] | None = None, - search: Annotated[str, - "Optional filter string for 'list' (e.g., 'Save')"] | None = None, - refresh: Annotated[bool, - "Optional flag to force refresh of the menu cache when listing"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing manage_menu_item: {action}") - # Prepare parameters for the C# handler - params_dict: dict[str, Any] = { - "action": action, - "menuPath": menu_path, - "search": search, - "refresh": refresh, - } - # Remove None values - params_dict = {k: v for k, v in params_dict.items() if v is not None} +@mcp_for_unity_tool( + description="Manage Unity menu items (execute/list/exists). If you're not sure what menu item to use, use the 'list' action to find it before using 'execute'." +) +async def manage_menu_item( + ctx: Context, + action: Annotated[Literal["execute", "list", "exists"], "Read and execute Unity menu items."], + menu_path: Annotated[str, + "Menu path for 'execute' or 'exists' (e.g., 'File/Save Project')"] | None = None, + search: Annotated[str, + "Optional filter string for 'list' (e.g., 'Save')"] | None = None, + refresh: Annotated[bool, + "Optional flag to force refresh of the menu cache when listing"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_menu_item: {action}") + # Prepare parameters for the C# handler + params_dict: dict[str, Any] = { + "action": action, + "menuPath": menu_path, + "search": search, + "refresh": refresh, + } + # Remove None values + params_dict = {k: v for k, v in params_dict.items() if v is not None} - # Get the current asyncio event loop - loop = asyncio.get_running_loop() + # Get the current asyncio event loop + loop = asyncio.get_running_loop() - # Use centralized async retry helper - result = await async_send_command_with_retry("manage_menu_item", params_dict, loop=loop) - return result if isinstance(result, dict) else {"success": False, "message": str(result)} + # Use centralized async retry helper + result = await async_send_command_with_retry("manage_menu_item", params_dict, loop=loop) + return result if isinstance(result, dict) else {"success": False, "message": str(result)} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_prefabs.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_prefabs.py index 7c65f28f..ea89201c 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_prefabs.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_prefabs.py @@ -1,61 +1,58 @@ from typing import Annotated, Any, Literal -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool - +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry -def register_manage_prefabs_tools(mcp: FastMCP) -> None: - """Register prefab management tools with the MCP server.""" - - @mcp.tool(name="manage_prefabs", description="Bridge for prefab management commands (stage control and creation).") - @telemetry_tool("manage_prefabs") - def manage_prefabs( - ctx: Context, - action: Annotated[Literal[ - "open_stage", - "close_stage", - "save_open_stage", - "create_from_gameobject", - ], "Manage prefabs (stage control and creation)."], - prefab_path: Annotated[str, - "Prefab asset path relative to Assets e.g. Assets/Prefabs/favorite.prefab"] | None = None, - mode: Annotated[str, - "Optional prefab stage mode (only 'InIsolation' is currently supported)"] | None = None, - save_before_close: Annotated[bool, - "When true, `close_stage` will save the prefab before exiting the stage."] | None = None, - target: Annotated[str, - "Scene GameObject name required for create_from_gameobject"] | None = None, - allow_overwrite: Annotated[bool, - "Allow replacing an existing prefab at the same path"] | None = None, - search_inactive: Annotated[bool, - "Include inactive objects when resolving the target name"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing manage_prefabs: {action}") - try: - params: dict[str, Any] = {"action": action} +@mcp_for_unity_tool( + description="Bridge for prefab management commands (stage control and creation)." +) +def manage_prefabs( + ctx: Context, + action: Annotated[Literal[ + "open_stage", + "close_stage", + "save_open_stage", + "create_from_gameobject", + ], "Manage prefabs (stage control and creation)."], + prefab_path: Annotated[str, + "Prefab asset path relative to Assets e.g. Assets/Prefabs/favorite.prefab"] | None = None, + mode: Annotated[str, + "Optional prefab stage mode (only 'InIsolation' is currently supported)"] | None = None, + save_before_close: Annotated[bool, + "When true, `close_stage` will save the prefab before exiting the stage."] | None = None, + target: Annotated[str, + "Scene GameObject name required for create_from_gameobject"] | None = None, + allow_overwrite: Annotated[bool, + "Allow replacing an existing prefab at the same path"] | None = None, + search_inactive: Annotated[bool, + "Include inactive objects when resolving the target name"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_prefabs: {action}") + try: + params: dict[str, Any] = {"action": action} - if prefab_path: - params["prefabPath"] = prefab_path - if mode: - params["mode"] = mode - if save_before_close is not None: - params["saveBeforeClose"] = bool(save_before_close) - if target: - params["target"] = target - if allow_overwrite is not None: - params["allowOverwrite"] = bool(allow_overwrite) - if search_inactive is not None: - params["searchInactive"] = bool(search_inactive) - response = send_command_with_retry("manage_prefabs", params) + if prefab_path: + params["prefabPath"] = prefab_path + if mode: + params["mode"] = mode + if save_before_close is not None: + params["saveBeforeClose"] = bool(save_before_close) + if target: + params["target"] = target + if allow_overwrite is not None: + params["allowOverwrite"] = bool(allow_overwrite) + if search_inactive is not None: + params["searchInactive"] = bool(search_inactive) + response = send_command_with_retry("manage_prefabs", params) - if isinstance(response, dict) and response.get("success"): - return { - "success": True, - "message": response.get("message", "Prefab operation successful."), - "data": response.get("data"), - } - return response if isinstance(response, dict) else {"success": False, "message": str(response)} - except Exception as exc: - return {"success": False, "message": f"Python error managing prefabs: {exc}"} + if isinstance(response, dict) and response.get("success"): + return { + "success": True, + "message": response.get("message", "Prefab operation successful."), + "data": response.get("data"), + } + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + except Exception as exc: + return {"success": False, "message": f"Python error managing prefabs: {exc}"} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_scene.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_scene.py index fb5a1bca..09494e4a 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_scene.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_scene.py @@ -1,61 +1,56 @@ from typing import Annotated, Literal, Any -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool - +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry -def register_manage_scene_tools(mcp: FastMCP): - """Register all scene management tools with the MCP server.""" - - @mcp.tool(name="manage_scene", description="Manage Unity scenes") - @telemetry_tool("manage_scene") - def manage_scene( - ctx: Context, - action: Annotated[Literal["create", "load", "save", "get_hierarchy", "get_active", "get_build_settings"], "Perform CRUD operations on Unity scenes."], - name: Annotated[str, - "Scene name. Not required get_active/get_build_settings"] | None = None, - path: Annotated[str, - "Asset path for scene operations (default: 'Assets/')"] | None = None, - build_index: Annotated[int, - "Build index for load/build settings actions"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing manage_scene: {action}") - try: - # Coerce numeric inputs defensively - def _coerce_int(value, default=None): - if value is None: +@mcp_for_unity_tool(description="Manage Unity scenes") +def manage_scene( + ctx: Context, + action: Annotated[Literal["create", "load", "save", "get_hierarchy", "get_active", "get_build_settings"], "Perform CRUD operations on Unity scenes."], + name: Annotated[str, + "Scene name. Not required get_active/get_build_settings"] | None = None, + path: Annotated[str, + "Asset path for scene operations (default: 'Assets/')"] | None = None, + build_index: Annotated[int, + "Build index for load/build settings actions"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_scene: {action}") + try: + # Coerce numeric inputs defensively + def _coerce_int(value, default=None): + if value is None: + return default + try: + if isinstance(value, bool): return default - try: - if isinstance(value, bool): - return default - if isinstance(value, int): - return int(value) - s = str(value).strip() - if s.lower() in ("", "none", "null"): - return default - return int(float(s)) - except Exception: + if isinstance(value, int): + return int(value) + s = str(value).strip() + if s.lower() in ("", "none", "null"): return default - - coerced_build_index = _coerce_int(build_index, default=None) - - params = {"action": action} - if name: - params["name"] = name - if path: - params["path"] = path - if coerced_build_index is not None: - params["buildIndex"] = coerced_build_index - - # Use centralized retry helper - response = send_command_with_retry("manage_scene", params) - - # Preserve structured failure data; unwrap success into a friendlier shape - if isinstance(response, dict) and response.get("success"): - return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")} - return response if isinstance(response, dict) else {"success": False, "message": str(response)} - - except Exception as e: - return {"success": False, "message": f"Python error managing scene: {str(e)}"} + return int(float(s)) + except Exception: + return default + + coerced_build_index = _coerce_int(build_index, default=None) + + params = {"action": action} + if name: + params["name"] = name + if path: + params["path"] = path + if coerced_build_index is not None: + params["buildIndex"] = coerced_build_index + + # Use centralized retry helper + response = send_command_with_retry("manage_scene", params) + + # Preserve structured failure data; unwrap success into a friendlier shape + if isinstance(response, dict) and response.get("success"): + return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")} + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + + except Exception as e: + return {"success": False, "message": f"Python error managing scene: {str(e)}"} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py index fef1e92d..cad6a88c 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py @@ -5,561 +5,548 @@ from mcp.server.fastmcp import FastMCP, Context +from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry -try: - from telemetry_decorator import telemetry_tool - HAS_TELEMETRY = True -except ImportError: - HAS_TELEMETRY = False - - def telemetry_tool(tool_name: str): - def decorator(func): - return func - return decorator - - -def register_manage_script_tools(mcp: FastMCP): - """Register all script management tools with the MCP server.""" - - def _split_uri(uri: str) -> tuple[str, str]: - """Split an incoming URI or path into (name, directory) suitable for Unity. - - Rules: - - unity://path/Assets/... → keep as Assets-relative (after decode/normalize) - - file://... → percent-decode, normalize, strip host and leading slashes, - then, if any 'Assets' segment exists, return path relative to that 'Assets' root. - Otherwise, fall back to original name/dir behavior. - - plain paths → decode/normalize separators; if they contain an 'Assets' segment, - return relative to 'Assets'. - """ - raw_path: str - if uri.startswith("unity://path/"): - raw_path = uri[len("unity://path/"):] - elif uri.startswith("file://"): - parsed = urlparse(uri) - host = (parsed.netloc or "").strip() - p = parsed.path or "" - # UNC: file://server/share/... -> //server/share/... - if host and host.lower() != "localhost": - p = f"//{host}{p}" - # Use percent-decoded path, preserving leading slashes - raw_path = unquote(p) - else: - raw_path = uri - - # Percent-decode any residual encodings and normalize separators - raw_path = unquote(raw_path).replace("\\", "/") - # Strip leading slash only for Windows drive-letter forms like "/C:/..." - if os.name == "nt" and len(raw_path) >= 3 and raw_path[0] == "/" and raw_path[2] == ":": - raw_path = raw_path[1:] - - # Normalize path (collapse ../, ./) - norm = os.path.normpath(raw_path).replace("\\", "/") - - # If an 'Assets' segment exists, compute path relative to it (case-insensitive) - parts = [p for p in norm.split("/") if p not in ("", ".")] - idx = next((i for i, seg in enumerate(parts) - if seg.lower() == "assets"), None) - assets_rel = "/".join(parts[idx:]) if idx is not None else None - - effective_path = assets_rel if assets_rel else norm - # For POSIX absolute paths outside Assets, drop the leading '/' - # to return a clean relative-like directory (e.g., '/tmp' -> 'tmp'). - if effective_path.startswith("/"): - effective_path = effective_path[1:] - - name = os.path.splitext(os.path.basename(effective_path))[0] - directory = os.path.dirname(effective_path) - return name, directory - - @mcp.tool(name="apply_text_edits", description=( - """Apply small text edits to a C# script identified by URI. - IMPORTANT: This tool replaces EXACT character positions. Always verify content at target lines/columns BEFORE editing! - RECOMMENDED WORKFLOW: - 1. First call resources/read with start_line/line_count to verify exact content - 2. Count columns carefully (or use find_in_file to locate patterns) - 3. Apply your edit with precise coordinates - 4. Consider script_apply_edits with anchors for safer pattern-based replacements - Notes: - - For method/class operations, use script_apply_edits (safer, structured edits) - - For pattern-based replacements, consider anchor operations in script_apply_edits - - Lines, columns are 1-indexed - - Tabs count as 1 column""" - )) - @telemetry_tool("apply_text_edits") - def apply_text_edits( - ctx: Context, - uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."], - edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script, i.e. a list of {startLine,startCol,endLine,endCol,newText} (1-indexed!)"], - precondition_sha256: Annotated[str, - "Optional SHA256 of the script to edit, used to prevent concurrent edits"] | None = None, - strict: Annotated[bool, - "Optional strict flag, used to enforce strict mode"] | None = None, - options: Annotated[dict[str, Any], - "Optional options, used to pass additional options to the script editor"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing apply_text_edits: {uri}") - name, directory = _split_uri(uri) - # Normalize common aliases/misuses for resilience: - # - Accept LSP-style range objects: {range:{start:{line,character}, end:{...}}, newText|text} - # - Accept index ranges as a 2-int array: {range:[startIndex,endIndex], text} - # If normalization is required, read current contents to map indices -> 1-based line/col. - def _needs_normalization(arr: list[dict[str, Any]]) -> bool: - for e in arr or []: - if ("startLine" not in e) or ("startCol" not in e) or ("endLine" not in e) or ("endCol" not in e) or ("newText" not in e and "text" in e): - return True - return False - - normalized_edits: list[dict[str, Any]] = [] - warnings: list[str] = [] - if _needs_normalization(edits): - # Read file to support index->line/col conversion when needed - read_resp = send_command_with_retry("manage_script", { - "action": "read", - "name": name, - "path": directory, - }) - if not (isinstance(read_resp, dict) and read_resp.get("success")): - return read_resp if isinstance(read_resp, dict) else {"success": False, "message": str(read_resp)} - data = read_resp.get("data", {}) - contents = data.get("contents") - if not contents and data.get("contentsEncoded"): - try: - contents = base64.b64decode(data.get("encodedContents", "").encode( - "utf-8")).decode("utf-8", "replace") - except Exception: - contents = contents or "" - - # Helper to map 0-based character index to 1-based line/col - def line_col_from_index(idx: int) -> tuple[int, int]: - if idx <= 0: - return 1, 1 - # Count lines up to idx and position within line - nl_count = contents.count("\n", 0, idx) - line = nl_count + 1 - last_nl = contents.rfind("\n", 0, idx) - col = (idx - (last_nl + 1)) + 1 if last_nl >= 0 else idx + 1 - return line, col - - for e in edits or []: - e2 = dict(e) - # Map text->newText if needed - if "newText" not in e2 and "text" in e2: - e2["newText"] = e2.pop("text") - - if "startLine" in e2 and "startCol" in e2 and "endLine" in e2 and "endCol" in e2: - # Guard: explicit fields must be 1-based. - zero_based = False +def _split_uri(uri: str) -> tuple[str, str]: + """Split an incoming URI or path into (name, directory) suitable for Unity. + + Rules: + - unity://path/Assets/... → keep as Assets-relative (after decode/normalize) + - file://... → percent-decode, normalize, strip host and leading slashes, + then, if any 'Assets' segment exists, return path relative to that 'Assets' root. + Otherwise, fall back to original name/dir behavior. + - plain paths → decode/normalize separators; if they contain an 'Assets' segment, + return relative to 'Assets'. + """ + raw_path: str + if uri.startswith("unity://path/"): + raw_path = uri[len("unity://path/"):] + elif uri.startswith("file://"): + parsed = urlparse(uri) + host = (parsed.netloc or "").strip() + p = parsed.path or "" + # UNC: file://server/share/... -> //server/share/... + if host and host.lower() != "localhost": + p = f"//{host}{p}" + # Use percent-decoded path, preserving leading slashes + raw_path = unquote(p) + else: + raw_path = uri + + # Percent-decode any residual encodings and normalize separators + raw_path = unquote(raw_path).replace("\\", "/") + # Strip leading slash only for Windows drive-letter forms like "/C:/..." + if os.name == "nt" and len(raw_path) >= 3 and raw_path[0] == "/" and raw_path[2] == ":": + raw_path = raw_path[1:] + + # Normalize path (collapse ../, ./) + norm = os.path.normpath(raw_path).replace("\\", "/") + + # If an 'Assets' segment exists, compute path relative to it (case-insensitive) + parts = [p for p in norm.split("/") if p not in ("", ".")] + idx = next((i for i, seg in enumerate(parts) + if seg.lower() == "assets"), None) + assets_rel = "/".join(parts[idx:]) if idx is not None else None + + effective_path = assets_rel if assets_rel else norm + # For POSIX absolute paths outside Assets, drop the leading '/' + # to return a clean relative-like directory (e.g., '/tmp' -> 'tmp'). + if effective_path.startswith("/"): + effective_path = effective_path[1:] + + name = os.path.splitext(os.path.basename(effective_path))[0] + directory = os.path.dirname(effective_path) + return name, directory + + +@mcp_for_unity_tool(description=( + """Apply small text edits to a C# script identified by URI. + IMPORTANT: This tool replaces EXACT character positions. Always verify content at target lines/columns BEFORE editing! + RECOMMENDED WORKFLOW: + 1. First call resources/read with start_line/line_count to verify exact content + 2. Count columns carefully (or use find_in_file to locate patterns) + 3. Apply your edit with precise coordinates + 4. Consider script_apply_edits with anchors for safer pattern-based replacements + Notes: + - For method/class operations, use script_apply_edits (safer, structured edits) + - For pattern-based replacements, consider anchor operations in script_apply_edits + - Lines, columns are 1-indexed + - Tabs count as 1 column""" +)) +def apply_text_edits( + ctx: Context, + uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."], + edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script, i.e. a list of {startLine,startCol,endLine,endCol,newText} (1-indexed!)"], + precondition_sha256: Annotated[str, + "Optional SHA256 of the script to edit, used to prevent concurrent edits"] | None = None, + strict: Annotated[bool, + "Optional strict flag, used to enforce strict mode"] | None = None, + options: Annotated[dict[str, Any], + "Optional options, used to pass additional options to the script editor"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing apply_text_edits: {uri}") + name, directory = _split_uri(uri) + + # Normalize common aliases/misuses for resilience: + # - Accept LSP-style range objects: {range:{start:{line,character}, end:{...}}, newText|text} + # - Accept index ranges as a 2-int array: {range:[startIndex,endIndex], text} + # If normalization is required, read current contents to map indices -> 1-based line/col. + def _needs_normalization(arr: list[dict[str, Any]]) -> bool: + for e in arr or []: + if ("startLine" not in e) or ("startCol" not in e) or ("endLine" not in e) or ("endCol" not in e) or ("newText" not in e and "text" in e): + return True + return False + + normalized_edits: list[dict[str, Any]] = [] + warnings: list[str] = [] + if _needs_normalization(edits): + # Read file to support index->line/col conversion when needed + read_resp = send_command_with_retry("manage_script", { + "action": "read", + "name": name, + "path": directory, + }) + if not (isinstance(read_resp, dict) and read_resp.get("success")): + return read_resp if isinstance(read_resp, dict) else {"success": False, "message": str(read_resp)} + data = read_resp.get("data", {}) + contents = data.get("contents") + if not contents and data.get("contentsEncoded"): + try: + contents = base64.b64decode(data.get("encodedContents", "").encode( + "utf-8")).decode("utf-8", "replace") + except Exception: + contents = contents or "" + + # Helper to map 0-based character index to 1-based line/col + def line_col_from_index(idx: int) -> tuple[int, int]: + if idx <= 0: + return 1, 1 + # Count lines up to idx and position within line + nl_count = contents.count("\n", 0, idx) + line = nl_count + 1 + last_nl = contents.rfind("\n", 0, idx) + col = (idx - (last_nl + 1)) + 1 if last_nl >= 0 else idx + 1 + return line, col + + for e in edits or []: + e2 = dict(e) + # Map text->newText if needed + if "newText" not in e2 and "text" in e2: + e2["newText"] = e2.pop("text") + + if "startLine" in e2 and "startCol" in e2 and "endLine" in e2 and "endCol" in e2: + # Guard: explicit fields must be 1-based. + zero_based = False + for k in ("startLine", "startCol", "endLine", "endCol"): + try: + if int(e2.get(k, 1)) < 1: + zero_based = True + except Exception: + pass + if zero_based: + if strict: + return {"success": False, "code": "zero_based_explicit_fields", "message": "Explicit line/col fields are 1-based; received zero-based.", "data": {"normalizedEdits": normalized_edits}} + # Normalize by clamping to 1 and warn for k in ("startLine", "startCol", "endLine", "endCol"): try: if int(e2.get(k, 1)) < 1: - zero_based = True + e2[k] = 1 except Exception: pass - if zero_based: - if strict: - return {"success": False, "code": "zero_based_explicit_fields", "message": "Explicit line/col fields are 1-based; received zero-based.", "data": {"normalizedEdits": normalized_edits}} - # Normalize by clamping to 1 and warn - for k in ("startLine", "startCol", "endLine", "endCol"): - try: - if int(e2.get(k, 1)) < 1: - e2[k] = 1 - except Exception: - pass - warnings.append( - "zero_based_explicit_fields_normalized") - normalized_edits.append(e2) - continue - - rng = e2.get("range") - if isinstance(rng, dict): - # LSP style: 0-based - s = rng.get("start", {}) - t = rng.get("end", {}) - e2["startLine"] = int(s.get("line", 0)) + 1 - e2["startCol"] = int(s.get("character", 0)) + 1 - e2["endLine"] = int(t.get("line", 0)) + 1 - e2["endCol"] = int(t.get("character", 0)) + 1 + warnings.append( + "zero_based_explicit_fields_normalized") + normalized_edits.append(e2) + continue + + rng = e2.get("range") + if isinstance(rng, dict): + # LSP style: 0-based + s = rng.get("start", {}) + t = rng.get("end", {}) + e2["startLine"] = int(s.get("line", 0)) + 1 + e2["startCol"] = int(s.get("character", 0)) + 1 + e2["endLine"] = int(t.get("line", 0)) + 1 + e2["endCol"] = int(t.get("character", 0)) + 1 + e2.pop("range", None) + normalized_edits.append(e2) + continue + if isinstance(rng, (list, tuple)) and len(rng) == 2: + try: + a = int(rng[0]) + b = int(rng[1]) + if b < a: + a, b = b, a + sl, sc = line_col_from_index(a) + el, ec = line_col_from_index(b) + e2["startLine"] = sl + e2["startCol"] = sc + e2["endLine"] = el + e2["endCol"] = ec e2.pop("range", None) normalized_edits.append(e2) continue - if isinstance(rng, (list, tuple)) and len(rng) == 2: + except Exception: + pass + # Could not normalize this edit + return { + "success": False, + "code": "missing_field", + "message": "apply_text_edits requires startLine/startCol/endLine/endCol/newText or a normalizable 'range'", + "data": {"expected": ["startLine", "startCol", "endLine", "endCol", "newText"], "got": e} + } + else: + # Even when edits appear already in explicit form, validate 1-based coordinates. + normalized_edits = [] + for e in edits or []: + e2 = dict(e) + has_all = all(k in e2 for k in ( + "startLine", "startCol", "endLine", "endCol")) + if has_all: + zero_based = False + for k in ("startLine", "startCol", "endLine", "endCol"): try: - a = int(rng[0]) - b = int(rng[1]) - if b < a: - a, b = b, a - sl, sc = line_col_from_index(a) - el, ec = line_col_from_index(b) - e2["startLine"] = sl - e2["startCol"] = sc - e2["endLine"] = el - e2["endCol"] = ec - e2.pop("range", None) - normalized_edits.append(e2) - continue + if int(e2.get(k, 1)) < 1: + zero_based = True except Exception: pass - # Could not normalize this edit - return { - "success": False, - "code": "missing_field", - "message": "apply_text_edits requires startLine/startCol/endLine/endCol/newText or a normalizable 'range'", - "data": {"expected": ["startLine", "startCol", "endLine", "endCol", "newText"], "got": e} - } - else: - # Even when edits appear already in explicit form, validate 1-based coordinates. - normalized_edits = [] - for e in edits or []: - e2 = dict(e) - has_all = all(k in e2 for k in ( - "startLine", "startCol", "endLine", "endCol")) - if has_all: - zero_based = False + if zero_based: + if strict: + return {"success": False, "code": "zero_based_explicit_fields", "message": "Explicit line/col fields are 1-based; received zero-based.", "data": {"normalizedEdits": [e2]}} for k in ("startLine", "startCol", "endLine", "endCol"): try: if int(e2.get(k, 1)) < 1: - zero_based = True + e2[k] = 1 except Exception: pass - if zero_based: - if strict: - return {"success": False, "code": "zero_based_explicit_fields", "message": "Explicit line/col fields are 1-based; received zero-based.", "data": {"normalizedEdits": [e2]}} - for k in ("startLine", "startCol", "endLine", "endCol"): - try: - if int(e2.get(k, 1)) < 1: - e2[k] = 1 - except Exception: - pass - if "zero_based_explicit_fields_normalized" not in warnings: - warnings.append( - "zero_based_explicit_fields_normalized") - normalized_edits.append(e2) - - # Preflight: detect overlapping ranges among normalized line/col spans - def _pos_tuple(e: dict[str, Any], key_start: bool) -> tuple[int, int]: - return ( - int(e.get("startLine", 1)) if key_start else int( - e.get("endLine", 1)), - int(e.get("startCol", 1)) if key_start else int( - e.get("endCol", 1)), - ) - - def _le(a: tuple[int, int], b: tuple[int, int]) -> bool: - return a[0] < b[0] or (a[0] == b[0] and a[1] <= b[1]) - - # Consider only true replace ranges (non-zero length). Pure insertions (zero-width) don't overlap. - spans = [] - for e in normalized_edits or []: - try: - s = _pos_tuple(e, True) - t = _pos_tuple(e, False) - if s != t: - spans.append((s, t)) - except Exception: - # If coordinates missing or invalid, let the server validate later - pass - - if spans: - spans_sorted = sorted(spans, key=lambda p: (p[0][0], p[0][1])) - for i in range(1, len(spans_sorted)): - prev_end = spans_sorted[i-1][1] - curr_start = spans_sorted[i][0] - # Overlap if prev_end > curr_start (strict), i.e., not prev_end <= curr_start - if not _le(prev_end, curr_start): - conflicts = [{ - "startA": {"line": spans_sorted[i-1][0][0], "col": spans_sorted[i-1][0][1]}, - "endA": {"line": spans_sorted[i-1][1][0], "col": spans_sorted[i-1][1][1]}, - "startB": {"line": spans_sorted[i][0][0], "col": spans_sorted[i][0][1]}, - "endB": {"line": spans_sorted[i][1][0], "col": spans_sorted[i][1][1]}, - }] - return {"success": False, "code": "overlap", "data": {"status": "overlap", "conflicts": conflicts}} - - # Note: Do not auto-compute precondition if missing; callers should supply it - # via mcp__unity__get_sha or a prior read. This avoids hidden extra calls and - # preserves existing call-count expectations in clients/tests. - - # Default options: for multi-span batches, prefer atomic to avoid mid-apply imbalance - opts: dict[str, Any] = dict(options or {}) + if "zero_based_explicit_fields_normalized" not in warnings: + warnings.append( + "zero_based_explicit_fields_normalized") + normalized_edits.append(e2) + + # Preflight: detect overlapping ranges among normalized line/col spans + def _pos_tuple(e: dict[str, Any], key_start: bool) -> tuple[int, int]: + return ( + int(e.get("startLine", 1)) if key_start else int( + e.get("endLine", 1)), + int(e.get("startCol", 1)) if key_start else int( + e.get("endCol", 1)), + ) + + def _le(a: tuple[int, int], b: tuple[int, int]) -> bool: + return a[0] < b[0] or (a[0] == b[0] and a[1] <= b[1]) + + # Consider only true replace ranges (non-zero length). Pure insertions (zero-width) don't overlap. + spans = [] + for e in normalized_edits or []: try: - if len(normalized_edits) > 1 and "applyMode" not in opts: - opts["applyMode"] = "atomic" + s = _pos_tuple(e, True) + t = _pos_tuple(e, False) + if s != t: + spans.append((s, t)) except Exception: + # If coordinates missing or invalid, let the server validate later pass - # Support optional debug preview for span-by-span simulation without write - if opts.get("debug_preview"): - try: - import difflib - # Apply locally to preview final result - lines = [] - # Build an indexable original from a read if we normalized from read; otherwise skip - prev = "" - # We cannot guarantee file contents here without a read; return normalized spans only - return { - "success": True, - "message": "Preview only (no write)", - "data": { - "normalizedEdits": normalized_edits, - "preview": True - } - } - except Exception as e: - return {"success": False, "code": "preview_failed", "message": f"debug_preview failed: {e}", "data": {"normalizedEdits": normalized_edits}} - params = { - "action": "apply_text_edits", - "name": name, - "path": directory, - "edits": normalized_edits, - "precondition_sha256": precondition_sha256, - "options": opts, - } - params = {k: v for k, v in params.items() if v is not None} - resp = send_command_with_retry("manage_script", params) - if isinstance(resp, dict): - data = resp.setdefault("data", {}) - data.setdefault("normalizedEdits", normalized_edits) - if warnings: - data.setdefault("warnings", warnings) - if resp.get("success") and (options or {}).get("force_sentinel_reload"): - # Optional: flip sentinel via menu if explicitly requested - try: - import threading - import time - import json - import glob - import os + if spans: + spans_sorted = sorted(spans, key=lambda p: (p[0][0], p[0][1])) + for i in range(1, len(spans_sorted)): + prev_end = spans_sorted[i-1][1] + curr_start = spans_sorted[i][0] + # Overlap if prev_end > curr_start (strict), i.e., not prev_end <= curr_start + if not _le(prev_end, curr_start): + conflicts = [{ + "startA": {"line": spans_sorted[i-1][0][0], "col": spans_sorted[i-1][0][1]}, + "endA": {"line": spans_sorted[i-1][1][0], "col": spans_sorted[i-1][1][1]}, + "startB": {"line": spans_sorted[i][0][0], "col": spans_sorted[i][0][1]}, + "endB": {"line": spans_sorted[i][1][0], "col": spans_sorted[i][1][1]}, + }] + return {"success": False, "code": "overlap", "data": {"status": "overlap", "conflicts": conflicts}} + + # Note: Do not auto-compute precondition if missing; callers should supply it + # via mcp__unity__get_sha or a prior read. This avoids hidden extra calls and + # preserves existing call-count expectations in clients/tests. + + # Default options: for multi-span batches, prefer atomic to avoid mid-apply imbalance + opts: dict[str, Any] = dict(options or {}) + try: + if len(normalized_edits) > 1 and "applyMode" not in opts: + opts["applyMode"] = "atomic" + except Exception: + pass + # Support optional debug preview for span-by-span simulation without write + if opts.get("debug_preview"): + try: + import difflib + # Apply locally to preview final result + lines = [] + # Build an indexable original from a read if we normalized from read; otherwise skip + prev = "" + # We cannot guarantee file contents here without a read; return normalized spans only + return { + "success": True, + "message": "Preview only (no write)", + "data": { + "normalizedEdits": normalized_edits, + "preview": True + } + } + except Exception as e: + return {"success": False, "code": "preview_failed", "message": f"debug_preview failed: {e}", "data": {"normalizedEdits": normalized_edits}} + + params = { + "action": "apply_text_edits", + "name": name, + "path": directory, + "edits": normalized_edits, + "precondition_sha256": precondition_sha256, + "options": opts, + } + params = {k: v for k, v in params.items() if v is not None} + resp = send_command_with_retry("manage_script", params) + if isinstance(resp, dict): + data = resp.setdefault("data", {}) + data.setdefault("normalizedEdits", normalized_edits) + if warnings: + data.setdefault("warnings", warnings) + if resp.get("success") and (options or {}).get("force_sentinel_reload"): + # Optional: flip sentinel via menu if explicitly requested + try: + import threading + import time + import json + import glob + import os - def _latest_status() -> dict | None: - try: - files = sorted(glob.glob(os.path.expanduser( - "~/.unity-mcp/unity-mcp-status-*.json")), key=os.path.getmtime, reverse=True) - if not files: - return None - with open(files[0], "r") as f: - return json.loads(f.read()) - except Exception: + def _latest_status() -> dict | None: + try: + files = sorted(glob.glob(os.path.expanduser( + "~/.unity-mcp/unity-mcp-status-*.json")), key=os.path.getmtime, reverse=True) + if not files: return None + with open(files[0], "r") as f: + return json.loads(f.read()) + except Exception: + return None - def _flip_async(): - try: - time.sleep(0.1) - st = _latest_status() - if st and st.get("reloading"): - return - send_command_with_retry( - "execute_menu_item", - {"menuPath": "MCP/Flip Reload Sentinel"}, - max_retries=0, - retry_ms=0, - ) - except Exception: - pass - threading.Thread(target=_flip_async, daemon=True).start() - except Exception: - pass - return resp + def _flip_async(): + try: + time.sleep(0.1) + st = _latest_status() + if st and st.get("reloading"): + return + send_command_with_retry( + "execute_menu_item", + {"menuPath": "MCP/Flip Reload Sentinel"}, + max_retries=0, + retry_ms=0, + ) + except Exception: + pass + threading.Thread(target=_flip_async, daemon=True).start() + except Exception: + pass return resp - return {"success": False, "message": str(resp)} - - @mcp.tool(name="create_script", description=("Create a new C# script at the given project path.")) - @telemetry_tool("create_script") - def create_script( - ctx: Context, - path: Annotated[str, "Path under Assets/ to create the script at, e.g., 'Assets/Scripts/My.cs'"], - contents: Annotated[str, "Contents of the script to create. Note, this is Base64 encoded over transport."], - script_type: Annotated[str, "Script type (e.g., 'C#')"] | None = None, - namespace: Annotated[str, "Namespace for the script"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing create_script: {path}") - name = os.path.splitext(os.path.basename(path))[0] - directory = os.path.dirname(path) - # Local validation to avoid round-trips on obviously bad input - norm_path = os.path.normpath( - (path or "").replace("\\", "/")).replace("\\", "/") - if not directory or directory.split("/")[0].lower() != "assets": - return {"success": False, "code": "path_outside_assets", "message": f"path must be under 'Assets/'; got '{path}'."} - if ".." in norm_path.split("/") or norm_path.startswith("/"): - return {"success": False, "code": "bad_path", "message": "path must not contain traversal or be absolute."} - if not name: - return {"success": False, "code": "bad_path", "message": "path must include a script file name."} - if not norm_path.lower().endswith(".cs"): - return {"success": False, "code": "bad_extension", "message": "script file must end with .cs."} - params: dict[str, Any] = { - "action": "create", + return resp + return {"success": False, "message": str(resp)} + + +@mcp_for_unity_tool(description=("Create a new C# script at the given project path.")) +def create_script( + ctx: Context, + path: Annotated[str, "Path under Assets/ to create the script at, e.g., 'Assets/Scripts/My.cs'"], + contents: Annotated[str, "Contents of the script to create. Note, this is Base64 encoded over transport."], + script_type: Annotated[str, "Script type (e.g., 'C#')"] | None = None, + namespace: Annotated[str, "Namespace for the script"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing create_script: {path}") + name = os.path.splitext(os.path.basename(path))[0] + directory = os.path.dirname(path) + # Local validation to avoid round-trips on obviously bad input + norm_path = os.path.normpath( + (path or "").replace("\\", "/")).replace("\\", "/") + if not directory or directory.split("/")[0].lower() != "assets": + return {"success": False, "code": "path_outside_assets", "message": f"path must be under 'Assets/'; got '{path}'."} + if ".." in norm_path.split("/") or norm_path.startswith("/"): + return {"success": False, "code": "bad_path", "message": "path must not contain traversal or be absolute."} + if not name: + return {"success": False, "code": "bad_path", "message": "path must include a script file name."} + if not norm_path.lower().endswith(".cs"): + return {"success": False, "code": "bad_extension", "message": "script file must end with .cs."} + params: dict[str, Any] = { + "action": "create", + "name": name, + "path": directory, + "namespace": namespace, + "scriptType": script_type, + } + if contents: + params["encodedContents"] = base64.b64encode( + contents.encode("utf-8")).decode("utf-8") + params["contentsEncoded"] = True + params = {k: v for k, v in params.items() if v is not None} + resp = send_command_with_retry("manage_script", params) + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + + +@mcp_for_unity_tool(description=("Delete a C# script by URI or Assets-relative path.")) +def delete_script( + ctx: Context, + uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] +) -> dict[str, Any]: + """Delete a C# script by URI.""" + ctx.info(f"Processing delete_script: {uri}") + name, directory = _split_uri(uri) + if not directory or directory.split("/")[0].lower() != "assets": + return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} + params = {"action": "delete", "name": name, "path": directory} + resp = send_command_with_retry("manage_script", params) + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + + +@mcp_for_unity_tool(description=("Validate a C# script and return diagnostics.")) +def validate_script( + ctx: Context, + uri: Annotated[str, "URI of the script to validate under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."], + level: Annotated[Literal['basic', 'standard'], + "Validation level"] = "basic", + include_diagnostics: Annotated[bool, + "Include full diagnostics and summary"] = False +) -> dict[str, Any]: + ctx.info(f"Processing validate_script: {uri}") + name, directory = _split_uri(uri) + if not directory or directory.split("/")[0].lower() != "assets": + return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} + if level not in ("basic", "standard"): + return {"success": False, "code": "bad_level", "message": "level must be 'basic' or 'standard'."} + params = { + "action": "validate", + "name": name, + "path": directory, + "level": level, + } + resp = send_command_with_retry("manage_script", params) + if isinstance(resp, dict) and resp.get("success"): + diags = resp.get("data", {}).get("diagnostics", []) or [] + warnings = sum(1 for d in diags if str( + d.get("severity", "")).lower() == "warning") + errors = sum(1 for d in diags if str( + d.get("severity", "")).lower() in ("error", "fatal")) + if include_diagnostics: + return {"success": True, "data": {"diagnostics": diags, "summary": {"warnings": warnings, "errors": errors}}} + return {"success": True, "data": {"warnings": warnings, "errors": errors}} + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + + +@mcp_for_unity_tool(description=("Compatibility router for legacy script operations. Prefer apply_text_edits (ranges) or script_apply_edits (structured) for edits.")) +def manage_script( + ctx: Context, + action: Annotated[Literal['create', 'read', 'delete'], "Perform CRUD operations on C# scripts."], + name: Annotated[str, "Script name (no .cs extension)", "Name of the script to create"], + path: Annotated[str, "Asset path (default: 'Assets/')", "Path under Assets/ to create the script at, e.g., 'Assets/Scripts/My.cs'"], + contents: Annotated[str, "Contents of the script to create", + "C# code for 'create'/'update'"] | None = None, + script_type: Annotated[str, "Script type (e.g., 'C#')", + "Type hint (e.g., 'MonoBehaviour')"] | None = None, + namespace: Annotated[str, "Namespace for the script"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_script: {action}") + try: + # Prepare parameters for Unity + params = { + "action": action, "name": name, - "path": directory, + "path": path, "namespace": namespace, "scriptType": script_type, } + + # Base64 encode the contents if they exist to avoid JSON escaping issues if contents: - params["encodedContents"] = base64.b64encode( - contents.encode("utf-8")).decode("utf-8") - params["contentsEncoded"] = True + if action == 'create': + params["encodedContents"] = base64.b64encode( + contents.encode('utf-8')).decode('utf-8') + params["contentsEncoded"] = True + else: + params["contents"] = contents + params = {k: v for k, v in params.items() if v is not None} - resp = send_command_with_retry("manage_script", params) - return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} - @mcp.tool(name="delete_script", description=("Delete a C# script by URI or Assets-relative path.")) - @telemetry_tool("delete_script") - def delete_script( - ctx: Context, - uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] - ) -> dict[str, Any]: - """Delete a C# script by URI.""" - ctx.info(f"Processing delete_script: {uri}") - name, directory = _split_uri(uri) - if not directory or directory.split("/")[0].lower() != "assets": - return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} - params = {"action": "delete", "name": name, "path": directory} - resp = send_command_with_retry("manage_script", params) - return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + response = send_command_with_retry("manage_script", params) - @mcp.tool(name="validate_script", description=("Validate a C# script and return diagnostics.")) - @telemetry_tool("validate_script") - def validate_script( - ctx: Context, - uri: Annotated[str, "URI of the script to validate under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."], - level: Annotated[Literal['basic', 'standard'], - "Validation level"] = "basic", - include_diagnostics: Annotated[bool, - "Include full diagnostics and summary"] = False - ) -> dict[str, Any]: - ctx.info(f"Processing validate_script: {uri}") - name, directory = _split_uri(uri) - if not directory or directory.split("/")[0].lower() != "assets": - return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} - if level not in ("basic", "standard"): - return {"success": False, "code": "bad_level", "message": "level must be 'basic' or 'standard'."} - params = { - "action": "validate", - "name": name, - "path": directory, - "level": level, - } - resp = send_command_with_retry("manage_script", params) - if isinstance(resp, dict) and resp.get("success"): - diags = resp.get("data", {}).get("diagnostics", []) or [] - warnings = sum(1 for d in diags if str( - d.get("severity", "")).lower() == "warning") - errors = sum(1 for d in diags if str( - d.get("severity", "")).lower() in ("error", "fatal")) - if include_diagnostics: - return {"success": True, "data": {"diagnostics": diags, "summary": {"warnings": warnings, "errors": errors}}} - return {"success": True, "data": {"warnings": warnings, "errors": errors}} - return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + if isinstance(response, dict): + if response.get("success"): + if response.get("data", {}).get("contentsEncoded"): + decoded_contents = base64.b64decode( + response["data"]["encodedContents"]).decode('utf-8') + response["data"]["contents"] = decoded_contents + del response["data"]["encodedContents"] + del response["data"]["contentsEncoded"] - @mcp.tool(name="manage_script", description=("Compatibility router for legacy script operations. Prefer apply_text_edits (ranges) or script_apply_edits (structured) for edits.")) - @telemetry_tool("manage_script") - def manage_script( - ctx: Context, - action: Annotated[Literal['create', 'read', 'delete'], "Perform CRUD operations on C# scripts."], - name: Annotated[str, "Script name (no .cs extension)", "Name of the script to create"], - path: Annotated[str, "Asset path (default: 'Assets/')", "Path under Assets/ to create the script at, e.g., 'Assets/Scripts/My.cs'"], - contents: Annotated[str, "Contents of the script to create", - "C# code for 'create'/'update'"] | None = None, - script_type: Annotated[str, "Script type (e.g., 'C#')", - "Type hint (e.g., 'MonoBehaviour')"] | None = None, - namespace: Annotated[str, "Namespace for the script"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing manage_script: {action}") - try: - # Prepare parameters for Unity - params = { - "action": action, - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type, - } + return { + "success": True, + "message": response.get("message", "Operation successful."), + "data": response.get("data"), + } + return response - # Base64 encode the contents if they exist to avoid JSON escaping issues - if contents: - if action == 'create': - params["encodedContents"] = base64.b64encode( - contents.encode('utf-8')).decode('utf-8') - params["contentsEncoded"] = True - else: - params["contents"] = contents - - params = {k: v for k, v in params.items() if v is not None} - - response = send_command_with_retry("manage_script", params) - - if isinstance(response, dict): - if response.get("success"): - if response.get("data", {}).get("contentsEncoded"): - decoded_contents = base64.b64decode( - response["data"]["encodedContents"]).decode('utf-8') - response["data"]["contents"] = decoded_contents - del response["data"]["encodedContents"] - del response["data"]["contentsEncoded"] - - return { - "success": True, - "message": response.get("message", "Operation successful."), - "data": response.get("data"), - } - return response - - return {"success": False, "message": str(response)} + return {"success": False, "message": str(response)} - except Exception as e: - return { - "success": False, - "message": f"Python error managing script: {str(e)}", - } + except Exception as e: + return { + "success": False, + "message": f"Python error managing script: {str(e)}", + } - @mcp.tool(name="manage_script_capabilities", description=( - """Get manage_script capabilities (supported ops, limits, and guards). - Returns: - - ops: list of supported structured ops - - text_ops: list of supported text ops - - max_edit_payload_bytes: server edit payload cap - - guards: header/using guard enabled flag""" - )) - @telemetry_tool("manage_script_capabilities") - def manage_script_capabilities(ctx: Context) -> dict[str, Any]: - ctx.info("Processing manage_script_capabilities") - try: - # Keep in sync with server/Editor ManageScript implementation - ops = [ - "replace_class", "delete_class", "replace_method", "delete_method", - "insert_method", "anchor_insert", "anchor_delete", "anchor_replace" - ] - text_ops = ["replace_range", "regex_replace", "prepend", "append"] - # Match ManageScript.MaxEditPayloadBytes if exposed; hardcode a sensible default fallback - max_edit_payload_bytes = 256 * 1024 - guards = {"using_guard": True} - extras = {"get_sha": True} - return {"success": True, "data": { - "ops": ops, - "text_ops": text_ops, - "max_edit_payload_bytes": max_edit_payload_bytes, - "guards": guards, - "extras": extras, - }} - except Exception as e: - return {"success": False, "error": f"capabilities error: {e}"} - - @mcp.tool(name="get_sha", description="Get SHA256 and basic metadata for a Unity C# script without returning file contents") - @telemetry_tool("get_sha") - def get_sha( - ctx: Context, - uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] - ) -> dict[str, Any]: - ctx.info(f"Processing get_sha: {uri}") - try: - name, directory = _split_uri(uri) - params = {"action": "get_sha", "name": name, "path": directory} - resp = send_command_with_retry("manage_script", params) - if isinstance(resp, dict) and resp.get("success"): - data = resp.get("data", {}) - minimal = {"sha256": data.get( - "sha256"), "lengthBytes": data.get("lengthBytes")} - return {"success": True, "data": minimal} - return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} - except Exception as e: - return {"success": False, "message": f"get_sha error: {e}"} + +@mcp_for_unity_tool(description=( + """Get manage_script capabilities (supported ops, limits, and guards). + Returns: + - ops: list of supported structured ops + - text_ops: list of supported text ops + - max_edit_payload_bytes: server edit payload cap + - guards: header/using guard enabled flag""" +)) +def manage_script_capabilities(ctx: Context) -> dict[str, Any]: + ctx.info("Processing manage_script_capabilities") + try: + # Keep in sync with server/Editor ManageScript implementation + ops = [ + "replace_class", "delete_class", "replace_method", "delete_method", + "insert_method", "anchor_insert", "anchor_delete", "anchor_replace" + ] + text_ops = ["replace_range", "regex_replace", "prepend", "append"] + # Match ManageScript.MaxEditPayloadBytes if exposed; hardcode a sensible default fallback + max_edit_payload_bytes = 256 * 1024 + guards = {"using_guard": True} + extras = {"get_sha": True} + return {"success": True, "data": { + "ops": ops, + "text_ops": text_ops, + "max_edit_payload_bytes": max_edit_payload_bytes, + "guards": guards, + "extras": extras, + }} + except Exception as e: + return {"success": False, "error": f"capabilities error: {e}"} + + +@mcp_for_unity_tool(description="Get SHA256 and basic metadata for a Unity C# script without returning file contents") +def get_sha( + ctx: Context, + uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] +) -> dict[str, Any]: + ctx.info(f"Processing get_sha: {uri}") + try: + name, directory = _split_uri(uri) + params = {"action": "get_sha", "name": name, "path": directory} + resp = send_command_with_retry("manage_script", params) + if isinstance(resp, dict) and resp.get("success"): + data = resp.get("data", {}) + minimal = {"sha256": data.get( + "sha256"), "lengthBytes": data.get("lengthBytes")} + return {"success": True, "data": minimal} + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + except Exception as e: + return {"success": False, "message": f"get_sha error: {e}"} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_script_edits.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_script_edits.py deleted file mode 100644 index 261eb502..00000000 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_script_edits.py +++ /dev/null @@ -1,968 +0,0 @@ -import base64 -import hashlib -import re -from typing import Annotated, Any - -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool - -from unity_connection import send_command_with_retry - - -def _apply_edits_locally(original_text: str, edits: list[dict[str, Any]]) -> str: - text = original_text - for edit in edits or []: - op = ( - (edit.get("op") - or edit.get("operation") - or edit.get("type") - or edit.get("mode") - or "") - .strip() - .lower() - ) - - if not op: - allowed = "anchor_insert, prepend, append, replace_range, regex_replace" - raise RuntimeError( - f"op is required; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation)." - ) - - if op == "prepend": - prepend_text = edit.get("text", "") - text = (prepend_text if prepend_text.endswith( - "\n") else prepend_text + "\n") + text - elif op == "append": - append_text = edit.get("text", "") - if not text.endswith("\n"): - text += "\n" - text += append_text - if not text.endswith("\n"): - text += "\n" - elif op == "anchor_insert": - anchor = edit.get("anchor", "") - position = (edit.get("position") or "before").lower() - insert_text = edit.get("text", "") - flags = re.MULTILINE | ( - re.IGNORECASE if edit.get("ignore_case") else 0) - - # Find the best match using improved heuristics - match = _find_best_anchor_match( - anchor, text, flags, bool(edit.get("prefer_last", True))) - if not match: - if edit.get("allow_noop", True): - continue - raise RuntimeError(f"anchor not found: {anchor}") - idx = match.start() if position == "before" else match.end() - text = text[:idx] + insert_text + text[idx:] - elif op == "replace_range": - start_line = int(edit.get("startLine", 1)) - start_col = int(edit.get("startCol", 1)) - end_line = int(edit.get("endLine", start_line)) - end_col = int(edit.get("endCol", 1)) - replacement = edit.get("text", "") - lines = text.splitlines(keepends=True) - max_line = len(lines) + 1 # 1-based, exclusive end - if (start_line < 1 or end_line < start_line or end_line > max_line - or start_col < 1 or end_col < 1): - raise RuntimeError("replace_range out of bounds") - - def index_of(line: int, col: int) -> int: - if line <= len(lines): - return sum(len(l) for l in lines[: line - 1]) + (col - 1) - return sum(len(l) for l in lines) - a = index_of(start_line, start_col) - b = index_of(end_line, end_col) - text = text[:a] + replacement + text[b:] - elif op == "regex_replace": - pattern = edit.get("pattern", "") - repl = edit.get("replacement", "") - # Translate $n backrefs (our input) to Python \g - repl_py = re.sub(r"\$(\d+)", r"\\g<\1>", repl) - count = int(edit.get("count", 0)) # 0 = replace all - flags = re.MULTILINE - if edit.get("ignore_case"): - flags |= re.IGNORECASE - text = re.sub(pattern, repl_py, text, count=count, flags=flags) - else: - allowed = "anchor_insert, prepend, append, replace_range, regex_replace" - raise RuntimeError( - f"unknown edit op: {op}; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation).") - return text - - -def _find_best_anchor_match(pattern: str, text: str, flags: int, prefer_last: bool = True): - """ - Find the best anchor match using improved heuristics. - - For patterns like \\s*}\\s*$ that are meant to find class-ending braces, - this function uses heuristics to choose the most semantically appropriate match: - - 1. If prefer_last=True, prefer the last match (common for class-end insertions) - 2. Use indentation levels to distinguish class vs method braces - 3. Consider context to avoid matches inside strings/comments - - Args: - pattern: Regex pattern to search for - text: Text to search in - flags: Regex flags - prefer_last: If True, prefer the last match over the first - - Returns: - Match object of the best match, or None if no match found - """ - - # Find all matches - matches = list(re.finditer(pattern, text, flags)) - if not matches: - return None - - # If only one match, return it - if len(matches) == 1: - return matches[0] - - # For patterns that look like they're trying to match closing braces at end of lines - is_closing_brace_pattern = '}' in pattern and ( - '$' in pattern or pattern.endswith(r'\s*')) - - if is_closing_brace_pattern and prefer_last: - # Use heuristics to find the best closing brace match - return _find_best_closing_brace_match(matches, text) - - # Default behavior: use last match if prefer_last, otherwise first match - return matches[-1] if prefer_last else matches[0] - - -def _find_best_closing_brace_match(matches, text: str): - """ - Find the best closing brace match using C# structure heuristics. - - Enhanced heuristics for scope-aware matching: - 1. Prefer matches with lower indentation (likely class-level) - 2. Prefer matches closer to end of file - 3. Avoid matches that seem to be inside method bodies - 4. For #endregion patterns, ensure class-level context - 5. Validate insertion point is at appropriate scope - - Args: - matches: List of regex match objects - text: The full text being searched - - Returns: - The best match object - """ - if not matches: - return None - - scored_matches = [] - lines = text.splitlines() - - for match in matches: - score = 0 - start_pos = match.start() - - # Find which line this match is on - lines_before = text[:start_pos].count('\n') - line_num = lines_before - - if line_num < len(lines): - line_content = lines[line_num] - - # Calculate indentation level (lower is better for class braces) - indentation = len(line_content) - len(line_content.lstrip()) - - # Prefer lower indentation (class braces are typically less indented than method braces) - # Max 20 points for indentation=0 - score += max(0, 20 - indentation) - - # Prefer matches closer to end of file (class closing braces are typically at the end) - distance_from_end = len(lines) - line_num - # More points for being closer to end - score += max(0, 10 - distance_from_end) - - # Look at surrounding context to avoid method braces - context_start = max(0, line_num - 3) - context_end = min(len(lines), line_num + 2) - context_lines = lines[context_start:context_end] - - # Penalize if this looks like it's inside a method (has method-like patterns above) - for context_line in context_lines: - if re.search(r'\b(void|public|private|protected)\s+\w+\s*\(', context_line): - score -= 5 # Penalty for being near method signatures - - # Bonus if this looks like a class-ending brace (very minimal indentation and near EOF) - if indentation <= 4 and distance_from_end <= 3: - score += 15 # Bonus for likely class-ending brace - - scored_matches.append((score, match)) - - # Return the match with the highest score - scored_matches.sort(key=lambda x: x[0], reverse=True) - best_match = scored_matches[0][1] - - return best_match - - -def _infer_class_name(script_name: str) -> str: - # Default to script name as class name (common Unity pattern) - return (script_name or "").strip() - - -def _extract_code_after(keyword: str, request: str) -> str: - # Deprecated with NL removal; retained as no-op for compatibility - idx = request.lower().find(keyword) - if idx >= 0: - return request[idx + len(keyword):].strip() - return "" -# Removed _is_structurally_balanced - validation now handled by C# side using Unity's compiler services - - -def _normalize_script_locator(name: str, path: str) -> tuple[str, str]: - """Best-effort normalization of script "name" and "path". - - Accepts any of: - - name = "SmartReach", path = "Assets/Scripts/Interaction" - - name = "SmartReach.cs", path = "Assets/Scripts/Interaction" - - name = "Assets/Scripts/Interaction/SmartReach.cs", path = "" - - path = "Assets/Scripts/Interaction/SmartReach.cs" (name empty) - - name or path using uri prefixes: unity://path/..., file://... - - accidental duplicates like "Assets/.../SmartReach.cs/SmartReach.cs" - - Returns (name_without_extension, directory_path_under_Assets). - """ - n = (name or "").strip() - p = (path or "").strip() - - def strip_prefix(s: str) -> str: - if s.startswith("unity://path/"): - return s[len("unity://path/"):] - if s.startswith("file://"): - return s[len("file://"):] - return s - - def collapse_duplicate_tail(s: str) -> str: - # Collapse trailing "/X.cs/X.cs" to "/X.cs" - parts = s.split("/") - if len(parts) >= 2 and parts[-1] == parts[-2]: - parts = parts[:-1] - return "/".join(parts) - - # Prefer a full path if provided in either field - candidate = "" - for v in (n, p): - v2 = strip_prefix(v) - if v2.endswith(".cs") or v2.startswith("Assets/"): - candidate = v2 - break - - if candidate: - candidate = collapse_duplicate_tail(candidate) - # If a directory was passed in path and file in name, join them - if not candidate.endswith(".cs") and n.endswith(".cs"): - v2 = strip_prefix(n) - candidate = (candidate.rstrip("/") + "/" + v2.split("/")[-1]) - if candidate.endswith(".cs"): - parts = candidate.split("/") - file_name = parts[-1] - dir_path = "/".join(parts[:-1]) if len(parts) > 1 else "Assets" - base = file_name[:- - 3] if file_name.lower().endswith(".cs") else file_name - return base, dir_path - - # Fall back: remove extension from name if present and return given path - base_name = n[:-3] if n.lower().endswith(".cs") else n - return base_name, (p or "Assets") - - -def _with_norm(resp: dict[str, Any] | Any, edits: list[dict[str, Any]], routing: str | None = None) -> dict[str, Any] | Any: - if not isinstance(resp, dict): - return resp - data = resp.setdefault("data", {}) - data.setdefault("normalizedEdits", edits) - if routing: - data["routing"] = routing - return resp - - -def _err(code: str, message: str, *, expected: dict[str, Any] | None = None, rewrite: dict[str, Any] | None = None, - normalized: list[dict[str, Any]] | None = None, routing: str | None = None, extra: dict[str, Any] | None = None) -> dict[str, Any]: - payload: dict[str, Any] = {"success": False, - "code": code, "message": message} - data: dict[str, Any] = {} - if expected: - data["expected"] = expected - if rewrite: - data["rewrite_suggestion"] = rewrite - if normalized is not None: - data["normalizedEdits"] = normalized - if routing: - data["routing"] = routing - if extra: - data.update(extra) - if data: - payload["data"] = data - return payload - -# Natural-language parsing removed; clients should send structured edits. - - -def register_manage_script_edits_tools(mcp: FastMCP): - @mcp.tool(name="script_apply_edits", description=( - """Structured C# edits (methods/classes) with safer boundaries - prefer this over raw text. - Best practices: - - Prefer anchor_* ops for pattern-based insert/replace near stable markers - - Use replace_method/delete_method for whole-method changes (keeps signatures balanced) - - Avoid whole-file regex deletes; validators will guard unbalanced braces - - For tail insertions, prefer anchor/regex_replace on final brace (class closing) - - Pass options.validate='standard' for structural checks; 'relaxed' for interior-only edits - Canonical fields (use these exact keys): - - op: replace_method | insert_method | delete_method | anchor_insert | anchor_delete | anchor_replace - - className: string (defaults to 'name' if omitted on method/class ops) - - methodName: string (required for replace_method, delete_method) - - replacement: string (required for replace_method, insert_method) - - position: start | end | after | before (insert_method only) - - afterMethodName / beforeMethodName: string (required when position='after'/'before') - - anchor: regex string (for anchor_* ops) - - text: string (for anchor_insert/anchor_replace) - Examples: - 1) Replace a method: - { - "name": "SmartReach", - "path": "Assets/Scripts/Interaction", - "edits": [ - { - "op": "replace_method", - "className": "SmartReach", - "methodName": "HasTarget", - "replacement": "public bool HasTarget(){ return currentTarget!=null; }" - } - ], - "options": {"validate": "standard", "refresh": "immediate"} - } - "2) Insert a method after another: - { - "name": "SmartReach", - "path": "Assets/Scripts/Interaction", - "edits": [ - { - "op": "insert_method", - "className": "SmartReach", - "replacement": "public void PrintSeries(){ Debug.Log(seriesName); }", - "position": "after", - "afterMethodName": "GetCurrentTarget" - } - ], - } - ]""" - )) - @telemetry_tool("script_apply_edits") - def script_apply_edits( - ctx: Context, - name: Annotated[str, "Name of the script to edit"], - path: Annotated[str, "Path to the script to edit under Assets/ directory"], - edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script"], - options: Annotated[dict[str, Any], - "Options for the script edit"] | None = None, - script_type: Annotated[str, - "Type of the script to edit"] = "MonoBehaviour", - namespace: Annotated[str, - "Namespace of the script to edit"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing script_apply_edits: {name}") - # Normalize locator first so downstream calls target the correct script file. - name, path = _normalize_script_locator(name, path) - # Normalize unsupported or aliased ops to known structured/text paths - - def _unwrap_and_alias(edit: dict[str, Any]) -> dict[str, Any]: - # Unwrap single-key wrappers like {"replace_method": {...}} - for wrapper_key in ( - "replace_method", "insert_method", "delete_method", - "replace_class", "delete_class", - "anchor_insert", "anchor_replace", "anchor_delete", - ): - if wrapper_key in edit and isinstance(edit[wrapper_key], dict): - inner = dict(edit[wrapper_key]) - inner["op"] = wrapper_key - edit = inner - break - - e = dict(edit) - op = (e.get("op") or e.get("operation") or e.get( - "type") or e.get("mode") or "").strip().lower() - if op: - e["op"] = op - - # Common field aliases - if "class_name" in e and "className" not in e: - e["className"] = e.pop("class_name") - if "class" in e and "className" not in e: - e["className"] = e.pop("class") - if "method_name" in e and "methodName" not in e: - e["methodName"] = e.pop("method_name") - # Some clients use a generic 'target' for method name - if "target" in e and "methodName" not in e: - e["methodName"] = e.pop("target") - if "method" in e and "methodName" not in e: - e["methodName"] = e.pop("method") - if "new_content" in e and "replacement" not in e: - e["replacement"] = e.pop("new_content") - if "newMethod" in e and "replacement" not in e: - e["replacement"] = e.pop("newMethod") - if "new_method" in e and "replacement" not in e: - e["replacement"] = e.pop("new_method") - if "content" in e and "replacement" not in e: - e["replacement"] = e.pop("content") - if "after" in e and "afterMethodName" not in e: - e["afterMethodName"] = e.pop("after") - if "after_method" in e and "afterMethodName" not in e: - e["afterMethodName"] = e.pop("after_method") - if "before" in e and "beforeMethodName" not in e: - e["beforeMethodName"] = e.pop("before") - if "before_method" in e and "beforeMethodName" not in e: - e["beforeMethodName"] = e.pop("before_method") - # anchor_method → before/after based on position (default after) - if "anchor_method" in e: - anchor = e.pop("anchor_method") - pos = (e.get("position") or "after").strip().lower() - if pos == "before" and "beforeMethodName" not in e: - e["beforeMethodName"] = anchor - elif "afterMethodName" not in e: - e["afterMethodName"] = anchor - if "anchorText" in e and "anchor" not in e: - e["anchor"] = e.pop("anchorText") - if "pattern" in e and "anchor" not in e and e.get("op") and e["op"].startswith("anchor_"): - e["anchor"] = e.pop("pattern") - if "newText" in e and "text" not in e: - e["text"] = e.pop("newText") - - # CI compatibility (T‑A/T‑E): - # Accept method-anchored anchor_insert and upgrade to insert_method - # Example incoming shape: - # {"op":"anchor_insert","afterMethodName":"GetCurrentTarget","text":"..."} - if ( - e.get("op") == "anchor_insert" - and not e.get("anchor") - and (e.get("afterMethodName") or e.get("beforeMethodName")) - ): - e["op"] = "insert_method" - if "replacement" not in e: - e["replacement"] = e.get("text", "") - - # LSP-like range edit -> replace_range - if "range" in e and isinstance(e["range"], dict): - rng = e.pop("range") - start = rng.get("start", {}) - end = rng.get("end", {}) - # Convert 0-based to 1-based line/col - e["op"] = "replace_range" - e["startLine"] = int(start.get("line", 0)) + 1 - e["startCol"] = int(start.get("character", 0)) + 1 - e["endLine"] = int(end.get("line", 0)) + 1 - e["endCol"] = int(end.get("character", 0)) + 1 - if "newText" in edit and "text" not in e: - e["text"] = edit.get("newText", "") - return e - - normalized_edits: list[dict[str, Any]] = [] - for raw in edits or []: - e = _unwrap_and_alias(raw) - op = (e.get("op") or e.get("operation") or e.get( - "type") or e.get("mode") or "").strip().lower() - - # Default className to script name if missing on structured method/class ops - if op in ("replace_class", "delete_class", "replace_method", "delete_method", "insert_method") and not e.get("className"): - e["className"] = name - - # Map common aliases for text ops - if op in ("text_replace",): - e["op"] = "replace_range" - normalized_edits.append(e) - continue - if op in ("regex_delete",): - e["op"] = "regex_replace" - e.setdefault("text", "") - normalized_edits.append(e) - continue - if op == "regex_replace" and ("replacement" not in e): - if "text" in e: - e["replacement"] = e.get("text", "") - elif "insert" in e or "content" in e: - e["replacement"] = e.get( - "insert") or e.get("content") or "" - if op == "anchor_insert" and not (e.get("text") or e.get("insert") or e.get("content") or e.get("replacement")): - e["op"] = "anchor_delete" - normalized_edits.append(e) - continue - normalized_edits.append(e) - - edits = normalized_edits - normalized_for_echo = edits - - # Validate required fields and produce machine-parsable hints - def error_with_hint(message: str, expected: dict[str, Any], suggestion: dict[str, Any]) -> dict[str, Any]: - return _err("missing_field", message, expected=expected, rewrite=suggestion, normalized=normalized_for_echo) - - for e in edits or []: - op = e.get("op", "") - if op == "replace_method": - if not e.get("methodName"): - return error_with_hint( - "replace_method requires 'methodName'.", - {"op": "replace_method", "required": [ - "className", "methodName", "replacement"]}, - {"edits[0].methodName": "HasTarget"} - ) - if not (e.get("replacement") or e.get("text")): - return error_with_hint( - "replace_method requires 'replacement' (inline or base64).", - {"op": "replace_method", "required": [ - "className", "methodName", "replacement"]}, - {"edits[0].replacement": "public bool X(){ return true; }"} - ) - elif op == "insert_method": - if not (e.get("replacement") or e.get("text")): - return error_with_hint( - "insert_method requires a non-empty 'replacement'.", - {"op": "insert_method", "required": ["className", "replacement"], "position": { - "after_requires": "afterMethodName", "before_requires": "beforeMethodName"}}, - {"edits[0].replacement": "public void PrintSeries(){ Debug.Log(\"1,2,3\"); }"} - ) - pos = (e.get("position") or "").lower() - if pos == "after" and not e.get("afterMethodName"): - return error_with_hint( - "insert_method with position='after' requires 'afterMethodName'.", - {"op": "insert_method", "position": { - "after_requires": "afterMethodName"}}, - {"edits[0].afterMethodName": "GetCurrentTarget"} - ) - if pos == "before" and not e.get("beforeMethodName"): - return error_with_hint( - "insert_method with position='before' requires 'beforeMethodName'.", - {"op": "insert_method", "position": { - "before_requires": "beforeMethodName"}}, - {"edits[0].beforeMethodName": "GetCurrentTarget"} - ) - elif op == "delete_method": - if not e.get("methodName"): - return error_with_hint( - "delete_method requires 'methodName'.", - {"op": "delete_method", "required": [ - "className", "methodName"]}, - {"edits[0].methodName": "PrintSeries"} - ) - elif op in ("anchor_insert", "anchor_replace", "anchor_delete"): - if not e.get("anchor"): - return error_with_hint( - f"{op} requires 'anchor' (regex).", - {"op": op, "required": ["anchor"]}, - {"edits[0].anchor": "(?m)^\\s*public\\s+bool\\s+HasTarget\\s*\\("} - ) - if op in ("anchor_insert", "anchor_replace") and not (e.get("text") or e.get("replacement")): - return error_with_hint( - f"{op} requires 'text'.", - {"op": op, "required": ["anchor", "text"]}, - {"edits[0].text": "/* comment */\n"} - ) - - # Decide routing: structured vs text vs mixed - STRUCT = {"replace_class", "delete_class", "replace_method", "delete_method", - "insert_method", "anchor_delete", "anchor_replace", "anchor_insert"} - TEXT = {"prepend", "append", "replace_range", "regex_replace"} - ops_set = {(e.get("op") or "").lower() for e in edits or []} - all_struct = ops_set.issubset(STRUCT) - all_text = ops_set.issubset(TEXT) - mixed = not (all_struct or all_text) - - # If everything is structured (method/class/anchor ops), forward directly to Unity's structured editor. - if all_struct: - opts2 = dict(options or {}) - # For structured edits, prefer immediate refresh to avoid missed reloads when Editor is unfocused - opts2.setdefault("refresh", "immediate") - params_struct: dict[str, Any] = { - "action": "edit", - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type, - "edits": edits, - "options": opts2, - } - resp_struct = send_command_with_retry( - "manage_script", params_struct) - if isinstance(resp_struct, dict) and resp_struct.get("success"): - pass # Optional sentinel reload removed (deprecated) - return _with_norm(resp_struct if isinstance(resp_struct, dict) else {"success": False, "message": str(resp_struct)}, normalized_for_echo, routing="structured") - - # 1) read from Unity - read_resp = send_command_with_retry("manage_script", { - "action": "read", - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type, - }) - if not isinstance(read_resp, dict) or not read_resp.get("success"): - return read_resp if isinstance(read_resp, dict) else {"success": False, "message": str(read_resp)} - - data = read_resp.get("data") or read_resp.get( - "result", {}).get("data") or {} - contents = data.get("contents") - if contents is None and data.get("contentsEncoded") and data.get("encodedContents"): - contents = base64.b64decode( - data["encodedContents"]).decode("utf-8") - if contents is None: - return {"success": False, "message": "No contents returned from Unity read."} - - # Optional preview/dry-run: apply locally and return diff without writing - preview = bool((options or {}).get("preview")) - - # If we have a mixed batch (TEXT + STRUCT), apply text first with precondition, then structured - if mixed: - text_edits = [e for e in edits or [] if ( - e.get("op") or "").lower() in TEXT] - struct_edits = [e for e in edits or [] if ( - e.get("op") or "").lower() in STRUCT] - try: - base_text = contents - - def line_col_from_index(idx: int) -> tuple[int, int]: - line = base_text.count("\n", 0, idx) + 1 - last_nl = base_text.rfind("\n", 0, idx) - col = (idx - (last_nl + 1)) + \ - 1 if last_nl >= 0 else idx + 1 - return line, col - - at_edits: list[dict[str, Any]] = [] - for e in text_edits: - opx = (e.get("op") or e.get("operation") or e.get( - "type") or e.get("mode") or "").strip().lower() - text_field = e.get("text") or e.get("insert") or e.get( - "content") or e.get("replacement") or "" - if opx == "anchor_insert": - anchor = e.get("anchor") or "" - position = (e.get("position") or "after").lower() - flags = re.MULTILINE | ( - re.IGNORECASE if e.get("ignore_case") else 0) - try: - # Use improved anchor matching logic - m = _find_best_anchor_match( - anchor, base_text, flags, prefer_last=True) - except Exception as ex: - return _with_norm(_err("bad_regex", f"Invalid anchor regex: {ex}", normalized=normalized_for_echo, routing="mixed/text-first", extra={"hint": "Escape parentheses/braces or use a simpler anchor."}), normalized_for_echo, routing="mixed/text-first") - if not m: - return _with_norm({"success": False, "code": "anchor_not_found", "message": f"anchor not found: {anchor}"}, normalized_for_echo, routing="mixed/text-first") - idx = m.start() if position == "before" else m.end() - # Normalize insertion to avoid jammed methods - text_field_norm = text_field - if not text_field_norm.startswith("\n"): - text_field_norm = "\n" + text_field_norm - if not text_field_norm.endswith("\n"): - text_field_norm = text_field_norm + "\n" - sl, sc = line_col_from_index(idx) - at_edits.append( - {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": text_field_norm}) - # do not mutate base_text when building atomic spans - elif opx == "replace_range": - if all(k in e for k in ("startLine", "startCol", "endLine", "endCol")): - at_edits.append({ - "startLine": int(e.get("startLine", 1)), - "startCol": int(e.get("startCol", 1)), - "endLine": int(e.get("endLine", 1)), - "endCol": int(e.get("endCol", 1)), - "newText": text_field - }) - else: - return _with_norm(_err("missing_field", "replace_range requires startLine/startCol/endLine/endCol", normalized=normalized_for_echo, routing="mixed/text-first"), normalized_for_echo, routing="mixed/text-first") - elif opx == "regex_replace": - pattern = e.get("pattern") or "" - try: - regex_obj = re.compile(pattern, re.MULTILINE | ( - re.IGNORECASE if e.get("ignore_case") else 0)) - except Exception as ex: - return _with_norm(_err("bad_regex", f"Invalid regex pattern: {ex}", normalized=normalized_for_echo, routing="mixed/text-first", extra={"hint": "Escape special chars or prefer structured delete for methods."}), normalized_for_echo, routing="mixed/text-first") - m = regex_obj.search(base_text) - if not m: - continue - # Expand $1, $2... in replacement using this match - - def _expand_dollars(rep: str, _m=m) -> str: - return re.sub(r"\$(\d+)", lambda g: _m.group(int(g.group(1))) or "", rep) - repl = _expand_dollars(text_field) - sl, sc = line_col_from_index(m.start()) - el, ec = line_col_from_index(m.end()) - at_edits.append( - {"startLine": sl, "startCol": sc, "endLine": el, "endCol": ec, "newText": repl}) - # do not mutate base_text when building atomic spans - elif opx in ("prepend", "append"): - if opx == "prepend": - sl, sc = 1, 1 - at_edits.append( - {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": text_field}) - # prepend can be applied atomically without local mutation - else: - # Insert at true EOF position (handles both \n and \r\n correctly) - eof_idx = len(base_text) - sl, sc = line_col_from_index(eof_idx) - new_text = ("\n" if not base_text.endswith( - "\n") else "") + text_field - at_edits.append( - {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": new_text}) - # do not mutate base_text when building atomic spans - else: - return _with_norm(_err("unknown_op", f"Unsupported text edit op: {opx}", normalized=normalized_for_echo, routing="mixed/text-first"), normalized_for_echo, routing="mixed/text-first") - - sha = hashlib.sha256(base_text.encode("utf-8")).hexdigest() - if at_edits: - params_text: dict[str, Any] = { - "action": "apply_text_edits", - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type, - "edits": at_edits, - "precondition_sha256": sha, - "options": {"refresh": (options or {}).get("refresh", "debounced"), "validate": (options or {}).get("validate", "standard"), "applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential"))} - } - resp_text = send_command_with_retry( - "manage_script", params_text) - if not (isinstance(resp_text, dict) and resp_text.get("success")): - return _with_norm(resp_text if isinstance(resp_text, dict) else {"success": False, "message": str(resp_text)}, normalized_for_echo, routing="mixed/text-first") - # Optional sentinel reload removed (deprecated) - except Exception as e: - return _with_norm({"success": False, "message": f"Text edit conversion failed: {e}"}, normalized_for_echo, routing="mixed/text-first") - - if struct_edits: - opts2 = dict(options or {}) - # Prefer debounced background refresh unless explicitly overridden - opts2.setdefault("refresh", "debounced") - params_struct: dict[str, Any] = { - "action": "edit", - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type, - "edits": struct_edits, - "options": opts2 - } - resp_struct = send_command_with_retry( - "manage_script", params_struct) - if isinstance(resp_struct, dict) and resp_struct.get("success"): - pass # Optional sentinel reload removed (deprecated) - return _with_norm(resp_struct if isinstance(resp_struct, dict) else {"success": False, "message": str(resp_struct)}, normalized_for_echo, routing="mixed/text-first") - - return _with_norm({"success": True, "message": "Applied text edits (no structured ops)"}, normalized_for_echo, routing="mixed/text-first") - - # If the edits are text-ops, prefer sending them to Unity's apply_text_edits with precondition - # so header guards and validation run on the C# side. - # Supported conversions: anchor_insert, replace_range, regex_replace (first match only). - text_ops = {(e.get("op") or e.get("operation") or e.get("type") or e.get( - "mode") or "").strip().lower() for e in (edits or [])} - structured_kinds = {"replace_class", "delete_class", - "replace_method", "delete_method", "insert_method", "anchor_insert"} - if not text_ops.issubset(structured_kinds): - # Convert to apply_text_edits payload - try: - base_text = contents - - def line_col_from_index(idx: int) -> tuple[int, int]: - # 1-based line/col against base buffer - line = base_text.count("\n", 0, idx) + 1 - last_nl = base_text.rfind("\n", 0, idx) - col = (idx - (last_nl + 1)) + \ - 1 if last_nl >= 0 else idx + 1 - return line, col - - at_edits: list[dict[str, Any]] = [] - import re as _re - for e in edits or []: - op = (e.get("op") or e.get("operation") or e.get( - "type") or e.get("mode") or "").strip().lower() - # aliasing for text field - text_field = e.get("text") or e.get( - "insert") or e.get("content") or "" - if op == "anchor_insert": - anchor = e.get("anchor") or "" - position = (e.get("position") or "after").lower() - # Use improved anchor matching logic with helpful errors, honoring ignore_case - try: - flags = re.MULTILINE | ( - re.IGNORECASE if e.get("ignore_case") else 0) - m = _find_best_anchor_match( - anchor, base_text, flags, prefer_last=True) - except Exception as ex: - return _with_norm(_err("bad_regex", f"Invalid anchor regex: {ex}", normalized=normalized_for_echo, routing="text", extra={"hint": "Escape parentheses/braces or use a simpler anchor."}), normalized_for_echo, routing="text") - if not m: - return _with_norm({"success": False, "code": "anchor_not_found", "message": f"anchor not found: {anchor}"}, normalized_for_echo, routing="text") - idx = m.start() if position == "before" else m.end() - # Normalize insertion newlines - if text_field and not text_field.startswith("\n"): - text_field = "\n" + text_field - if text_field and not text_field.endswith("\n"): - text_field = text_field + "\n" - sl, sc = line_col_from_index(idx) - at_edits.append({ - "startLine": sl, - "startCol": sc, - "endLine": sl, - "endCol": sc, - "newText": text_field or "" - }) - # Do not mutate base buffer when building an atomic batch - elif op == "replace_range": - # Directly forward if already in line/col form - if "startLine" in e: - at_edits.append({ - "startLine": int(e.get("startLine", 1)), - "startCol": int(e.get("startCol", 1)), - "endLine": int(e.get("endLine", 1)), - "endCol": int(e.get("endCol", 1)), - "newText": text_field - }) - else: - # If only indices provided, skip (we don't support index-based here) - return _with_norm({"success": False, "code": "missing_field", "message": "replace_range requires startLine/startCol/endLine/endCol"}, normalized_for_echo, routing="text") - elif op == "regex_replace": - pattern = e.get("pattern") or "" - repl = text_field - flags = re.MULTILINE | ( - re.IGNORECASE if e.get("ignore_case") else 0) - # Early compile for clearer error messages - try: - regex_obj = re.compile(pattern, flags) - except Exception as ex: - return _with_norm(_err("bad_regex", f"Invalid regex pattern: {ex}", normalized=normalized_for_echo, routing="text", extra={"hint": "Escape special chars or prefer structured delete for methods."}), normalized_for_echo, routing="text") - # Use smart anchor matching for consistent behavior with anchor_insert - m = _find_best_anchor_match( - pattern, base_text, flags, prefer_last=True) - if not m: - continue - # Expand $1, $2... backrefs in replacement using the first match (consistent with mixed-path behavior) - - def _expand_dollars(rep: str, _m=m) -> str: - return re.sub(r"\$(\d+)", lambda g: _m.group(int(g.group(1))) or "", rep) - repl_expanded = _expand_dollars(repl) - # Let C# side handle validation using Unity's built-in compiler services - sl, sc = line_col_from_index(m.start()) - el, ec = line_col_from_index(m.end()) - at_edits.append({ - "startLine": sl, - "startCol": sc, - "endLine": el, - "endCol": ec, - "newText": repl_expanded - }) - # Do not mutate base buffer when building an atomic batch - else: - return _with_norm({"success": False, "code": "unsupported_op", "message": f"Unsupported text edit op for server-side apply_text_edits: {op}"}, normalized_for_echo, routing="text") - - if not at_edits: - return _with_norm({"success": False, "code": "no_spans", "message": "No applicable text edit spans computed (anchor not found or zero-length)."}, normalized_for_echo, routing="text") - - sha = hashlib.sha256(base_text.encode("utf-8")).hexdigest() - params: dict[str, Any] = { - "action": "apply_text_edits", - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type, - "edits": at_edits, - "precondition_sha256": sha, - "options": { - "refresh": (options or {}).get("refresh", "debounced"), - "validate": (options or {}).get("validate", "standard"), - "applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential")) - } - } - resp = send_command_with_retry("manage_script", params) - if isinstance(resp, dict) and resp.get("success"): - pass # Optional sentinel reload removed (deprecated) - return _with_norm( - resp if isinstance(resp, dict) else { - "success": False, "message": str(resp)}, - normalized_for_echo, - routing="text" - ) - except Exception as e: - return _with_norm({"success": False, "code": "conversion_failed", "message": f"Edit conversion failed: {e}"}, normalized_for_echo, routing="text") - - # For regex_replace, honor preview consistently: if preview=true, always return diff without writing. - # If confirm=false (default) and preview not requested, return diff and instruct confirm=true to apply. - if "regex_replace" in text_ops and (preview or not (options or {}).get("confirm")): - try: - preview_text = _apply_edits_locally(contents, edits) - import difflib - diff = list(difflib.unified_diff(contents.splitlines( - ), preview_text.splitlines(), fromfile="before", tofile="after", n=2)) - if len(diff) > 800: - diff = diff[:800] + ["... (diff truncated) ..."] - if preview: - return {"success": True, "message": "Preview only (no write)", "data": {"diff": "\n".join(diff), "normalizedEdits": normalized_for_echo}} - return _with_norm({"success": False, "message": "Preview diff; set options.confirm=true to apply.", "data": {"diff": "\n".join(diff)}}, normalized_for_echo, routing="text") - except Exception as e: - return _with_norm({"success": False, "code": "preview_failed", "message": f"Preview failed: {e}"}, normalized_for_echo, routing="text") - # 2) apply edits locally (only if not text-ops) - try: - new_contents = _apply_edits_locally(contents, edits) - except Exception as e: - return {"success": False, "message": f"Edit application failed: {e}"} - - # Short-circuit no-op edits to avoid false "applied" reports downstream - if new_contents == contents: - return _with_norm({ - "success": True, - "message": "No-op: contents unchanged", - "data": {"no_op": True, "evidence": {"reason": "identical_content"}} - }, normalized_for_echo, routing="text") - - if preview: - # Produce a compact unified diff limited to small context - import difflib - a = contents.splitlines() - b = new_contents.splitlines() - diff = list(difflib.unified_diff( - a, b, fromfile="before", tofile="after", n=3)) - # Limit diff size to keep responses small - if len(diff) > 2000: - diff = diff[:2000] + ["... (diff truncated) ..."] - return {"success": True, "message": "Preview only (no write)", "data": {"diff": "\n".join(diff), "normalizedEdits": normalized_for_echo}} - - # 3) update to Unity - # Default refresh/validate for natural usage on text path as well - options = dict(options or {}) - options.setdefault("validate", "standard") - options.setdefault("refresh", "debounced") - - # Compute the SHA of the current file contents for the precondition - old_lines = contents.splitlines(keepends=True) - end_line = len(old_lines) + 1 # 1-based exclusive end - sha = hashlib.sha256(contents.encode("utf-8")).hexdigest() - - # Apply a whole-file text edit rather than the deprecated 'update' action - params = { - "action": "apply_text_edits", - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type, - "edits": [ - { - "startLine": 1, - "startCol": 1, - "endLine": end_line, - "endCol": 1, - "newText": new_contents, - } - ], - "precondition_sha256": sha, - "options": options or {"validate": "standard", "refresh": "debounced"}, - } - - write_resp = send_command_with_retry("manage_script", params) - if isinstance(write_resp, dict) and write_resp.get("success"): - pass # Optional sentinel reload removed (deprecated) - return _with_norm( - write_resp if isinstance(write_resp, dict) - else {"success": False, "message": str(write_resp)}, - normalized_for_echo, - routing="text", - ) diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_shader.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_shader.py index e9ccc14a..9c199661 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_shader.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_shader.py @@ -1,63 +1,60 @@ import base64 from typing import Annotated, Any, Literal -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool - +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry -def register_manage_shader_tools(mcp: FastMCP): - """Register all shader script management tools with the MCP server.""" - - @mcp.tool(name="manage_shader", description="Manages shader scripts in Unity (create, read, update, delete).") - @telemetry_tool("manage_shader") - def manage_shader( - ctx: Context, - action: Annotated[Literal['create', 'read', 'update', 'delete'], "Perform CRUD operations on shader scripts."], - name: Annotated[str, "Shader name (no .cs extension)"], - path: Annotated[str, "Asset path (default: \"Assets/\")"], - contents: Annotated[str, - "Shader code for 'create'/'update'"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing manage_shader: {action}") - try: - # Prepare parameters for Unity - params = { - "action": action, - "name": name, - "path": path, - } - - # Base64 encode the contents if they exist to avoid JSON escaping issues - if contents is not None: - if action in ['create', 'update']: - # Encode content for safer transmission - params["encodedContents"] = base64.b64encode( - contents.encode('utf-8')).decode('utf-8') - params["contentsEncoded"] = True - else: - params["contents"] = contents - - # Remove None values so they don't get sent as null - params = {k: v for k, v in params.items() if v is not None} - - # Send command via centralized retry helper - response = send_command_with_retry("manage_shader", params) - - # Process response from Unity - if isinstance(response, dict) and response.get("success"): - # If the response contains base64 encoded content, decode it - if response.get("data", {}).get("contentsEncoded"): - decoded_contents = base64.b64decode( - response["data"]["encodedContents"]).decode('utf-8') - response["data"]["contents"] = decoded_contents - del response["data"]["encodedContents"] - del response["data"]["contentsEncoded"] - - return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} - return response if isinstance(response, dict) else {"success": False, "message": str(response)} - - except Exception as e: - # Handle Python-side errors (e.g., connection issues) - return {"success": False, "message": f"Python error managing shader: {str(e)}"} +@mcp_for_unity_tool( + description="Manages shader scripts in Unity (create, read, update, delete)." +) +def manage_shader( + ctx: Context, + action: Annotated[Literal['create', 'read', 'update', 'delete'], "Perform CRUD operations on shader scripts."], + name: Annotated[str, "Shader name (no .cs extension)"], + path: Annotated[str, "Asset path (default: \"Assets/\")"], + contents: Annotated[str, + "Shader code for 'create'/'update'"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_shader: {action}") + try: + # Prepare parameters for Unity + params = { + "action": action, + "name": name, + "path": path, + } + + # Base64 encode the contents if they exist to avoid JSON escaping issues + if contents is not None: + if action in ['create', 'update']: + # Encode content for safer transmission + params["encodedContents"] = base64.b64encode( + contents.encode('utf-8')).decode('utf-8') + params["contentsEncoded"] = True + else: + params["contents"] = contents + + # Remove None values so they don't get sent as null + params = {k: v for k, v in params.items() if v is not None} + + # Send command via centralized retry helper + response = send_command_with_retry("manage_shader", params) + + # Process response from Unity + if isinstance(response, dict) and response.get("success"): + # If the response contains base64 encoded content, decode it + if response.get("data", {}).get("contentsEncoded"): + decoded_contents = base64.b64decode( + response["data"]["encodedContents"]).decode('utf-8') + response["data"]["contents"] = decoded_contents + del response["data"]["encodedContents"] + del response["data"]["contentsEncoded"] + + return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + + except Exception as e: + # Handle Python-side errors (e.g., connection issues) + return {"success": False, "message": f"Python error managing shader: {str(e)}"} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/read_console.py b/UnityMcpBridge/UnityMcpServer~/src/tools/read_console.py index c647cf8f..5fc9a096 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/read_console.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/read_console.py @@ -3,88 +3,85 @@ """ from typing import Annotated, Any, Literal -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool - +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry -def register_read_console_tools(mcp: FastMCP): - """Registers the read_console tool with the MCP server.""" - - @mcp.tool(name="read_console", description="Gets messages from or clears the Unity Editor console.") - @telemetry_tool("read_console") - def read_console( - ctx: Context, - action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."], - types: Annotated[list[Literal['error', 'warning', - 'log', 'all']], "Message types to get"] | None = None, - count: Annotated[int, "Max messages to return"] | None = None, - filter_text: Annotated[str, "Text filter for messages"] | None = None, - since_timestamp: Annotated[str, - "Get messages after this timestamp (ISO 8601)"] | None = None, - format: Annotated[Literal['plain', 'detailed', - 'json'], "Output format"] | None = None, - include_stacktrace: Annotated[bool, - "Include stack traces in output"] | None = None - ) -> dict[str, Any]: - ctx.info(f"Processing read_console: {action}") - # Set defaults if values are None - action = action if action is not None else 'get' - types = types if types is not None else ['error', 'warning', 'log'] - format = format if format is not None else 'detailed' - include_stacktrace = include_stacktrace if include_stacktrace is not None else True +@mcp_for_unity_tool( + description="Gets messages from or clears the Unity Editor console." +) +def read_console( + ctx: Context, + action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."], + types: Annotated[list[Literal['error', 'warning', + 'log', 'all']], "Message types to get"] | None = None, + count: Annotated[int, "Max messages to return"] | None = None, + filter_text: Annotated[str, "Text filter for messages"] | None = None, + since_timestamp: Annotated[str, + "Get messages after this timestamp (ISO 8601)"] | None = None, + format: Annotated[Literal['plain', 'detailed', + 'json'], "Output format"] | None = None, + include_stacktrace: Annotated[bool, + "Include stack traces in output"] | None = None +) -> dict[str, Any]: + ctx.info(f"Processing read_console: {action}") + # Set defaults if values are None + action = action if action is not None else 'get' + types = types if types is not None else ['error', 'warning', 'log'] + format = format if format is not None else 'detailed' + include_stacktrace = include_stacktrace if include_stacktrace is not None else True - # Normalize action if it's a string - if isinstance(action, str): - action = action.lower() + # Normalize action if it's a string + if isinstance(action, str): + action = action.lower() - # Coerce count defensively (string/float -> int) - def _coerce_int(value, default=None): - if value is None: + # Coerce count defensively (string/float -> int) + def _coerce_int(value, default=None): + if value is None: + return default + try: + if isinstance(value, bool): return default - try: - if isinstance(value, bool): - return default - if isinstance(value, int): - return int(value) - s = str(value).strip() - if s.lower() in ("", "none", "null"): - return default - return int(float(s)) - except Exception: + if isinstance(value, int): + return int(value) + s = str(value).strip() + if s.lower() in ("", "none", "null"): return default + return int(float(s)) + except Exception: + return default - count = _coerce_int(count) + count = _coerce_int(count) - # Prepare parameters for the C# handler - params_dict = { - "action": action, - "types": types, - "count": count, - "filterText": filter_text, - "sinceTimestamp": since_timestamp, - "format": format.lower() if isinstance(format, str) else format, - "includeStacktrace": include_stacktrace - } + # Prepare parameters for the C# handler + params_dict = { + "action": action, + "types": types, + "count": count, + "filterText": filter_text, + "sinceTimestamp": since_timestamp, + "format": format.lower() if isinstance(format, str) else format, + "includeStacktrace": include_stacktrace + } - # Remove None values unless it's 'count' (as None might mean 'all') - params_dict = {k: v for k, v in params_dict.items() - if v is not None or k == 'count'} + # Remove None values unless it's 'count' (as None might mean 'all') + params_dict = {k: v for k, v in params_dict.items() + if v is not None or k == 'count'} - # Add count back if it was None, explicitly sending null might be important for C# logic - if 'count' not in params_dict: - params_dict['count'] = None + # Add count back if it was None, explicitly sending null might be important for C# logic + if 'count' not in params_dict: + params_dict['count'] = None - # Use centralized retry helper - resp = send_command_with_retry("read_console", params_dict) - if isinstance(resp, dict) and resp.get("success") and not include_stacktrace: - # Strip stacktrace fields from returned lines if present - try: - lines = resp.get("data", {}).get("lines", []) - for line in lines: - if isinstance(line, dict) and "stacktrace" in line: - line.pop("stacktrace", None) - except Exception: - pass - return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + # Use centralized retry helper + resp = send_command_with_retry("read_console", params_dict) + if isinstance(resp, dict) and resp.get("success") and not include_stacktrace: + # Strip stacktrace fields from returned lines if present + try: + lines = resp.get("data", {}).get("lines", []) + for line in lines: + if isinstance(line, dict) and "stacktrace" in line: + line.pop("stacktrace", None) + except Exception: + pass + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/resource_tools.py b/UnityMcpBridge/UnityMcpServer~/src/tools/resource_tools.py index 2ae06e85..a8398f75 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/resource_tools.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/resource_tools.py @@ -11,9 +11,9 @@ from typing import Annotated, Any from urllib.parse import urlparse, unquote -from mcp.server.fastmcp import FastMCP, Context -from telemetry_decorator import telemetry_tool +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry @@ -133,264 +133,260 @@ def _resolve_safe_path_from_uri(uri: str, project: Path) -> Path | None: return p -def register_resource_tools(mcp: FastMCP) -> None: - """Registers list_resources and read_resource wrapper tools.""" - - @mcp.tool(name="list_resources", description=("List project URIs (unity://path/...) under a folder (default: Assets). Only .cs files are returned by default; always appends unity://spec/script-edits.\n")) - @telemetry_tool("list_resources") - async def list_resources( - ctx: Context, - pattern: Annotated[str, "Glob, default is *.cs"] | None = "*.cs", - under: Annotated[str, - "Folder under project root, default is Assets"] = "Assets", - limit: Annotated[int, "Page limit"] = 200, - project_root: Annotated[str, "Project path"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing list_resources: {pattern}") +@mcp_for_unity_tool(description=("List project URIs (unity://path/...) under a folder (default: Assets). Only .cs files are returned by default; always appends unity://spec/script-edits.\n")) +async def list_resources( + ctx: Context, + pattern: Annotated[str, "Glob, default is *.cs"] | None = "*.cs", + under: Annotated[str, + "Folder under project root, default is Assets"] = "Assets", + limit: Annotated[int, "Page limit"] = 200, + project_root: Annotated[str, "Project path"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing list_resources: {pattern}") + try: + project = _resolve_project_root(project_root) + base = (project / under).resolve() try: - project = _resolve_project_root(project_root) - base = (project / under).resolve() - try: - base.relative_to(project) - except ValueError: - return {"success": False, "error": "Base path must be under project root"} - # Enforce listing only under Assets + base.relative_to(project) + except ValueError: + return {"success": False, "error": "Base path must be under project root"} + # Enforce listing only under Assets + try: + base.relative_to(project / "Assets") + except ValueError: + return {"success": False, "error": "Listing is restricted to Assets/"} + + matches: list[str] = [] + limit_int = _coerce_int(limit, default=200, minimum=1) + for p in base.rglob("*"): + if not p.is_file(): + continue + # Resolve symlinks and ensure the real path stays under project/Assets try: - base.relative_to(project / "Assets") - except ValueError: - return {"success": False, "error": "Listing is restricted to Assets/"} + rp = p.resolve() + rp.relative_to(project / "Assets") + except Exception: + continue + # Enforce .cs extension regardless of provided pattern + if p.suffix.lower() != ".cs": + continue + if pattern and not fnmatch.fnmatch(p.name, pattern): + continue + rel = p.relative_to(project).as_posix() + matches.append(f"unity://path/{rel}") + if len(matches) >= max(1, limit_int): + break - matches: list[str] = [] - limit_int = _coerce_int(limit, default=200, minimum=1) - for p in base.rglob("*"): - if not p.is_file(): - continue - # Resolve symlinks and ensure the real path stays under project/Assets - try: - rp = p.resolve() - rp.relative_to(project / "Assets") - except Exception: - continue - # Enforce .cs extension regardless of provided pattern - if p.suffix.lower() != ".cs": - continue - if pattern and not fnmatch.fnmatch(p.name, pattern): - continue - rel = p.relative_to(project).as_posix() - matches.append(f"unity://path/{rel}") - if len(matches) >= max(1, limit_int): - break + # Always include the canonical spec resource so NL clients can discover it + if "unity://spec/script-edits" not in matches: + matches.append("unity://spec/script-edits") - # Always include the canonical spec resource so NL clients can discover it - if "unity://spec/script-edits" not in matches: - matches.append("unity://spec/script-edits") + return {"success": True, "data": {"uris": matches, "count": len(matches)}} + except Exception as e: + return {"success": False, "error": str(e)} - return {"success": True, "data": {"uris": matches, "count": len(matches)}} - except Exception as e: - return {"success": False, "error": str(e)} - @mcp.tool(name="read_resource", description=("Reads a resource by unity://path/... URI with optional slicing.")) - @telemetry_tool("read_resource") - async def read_resource( - ctx: Context, - uri: Annotated[str, "The resource URI to read under Assets/"], - start_line: Annotated[int, - "The starting line number (0-based)"] | None = None, - line_count: Annotated[int, - "The number of lines to read"] | None = None, - head_bytes: Annotated[int, - "The number of bytes to read from the start of the file"] | None = None, - tail_lines: Annotated[int, - "The number of lines to read from the end of the file"] | None = None, - project_root: Annotated[str, - "The project root directory"] | None = None, - request: Annotated[str, "The request ID"] | None = None, - ) -> dict[str, Any]: - ctx.info(f"Processing read_resource: {uri}") - try: - # Serve the canonical spec directly when requested (allow bare or with scheme) - if uri in ("unity://spec/script-edits", "spec/script-edits", "script-edits"): - spec_json = ( - '{\n' - ' "name": "Unity MCP - Script Edits v1",\n' - ' "target_tool": "script_apply_edits",\n' - ' "canonical_rules": {\n' - ' "always_use": ["op","className","methodName","replacement","afterMethodName","beforeMethodName"],\n' - ' "never_use": ["new_method","anchor_method","content","newText"],\n' - ' "defaults": {\n' - ' "className": "\u2190 server will default to \'name\' when omitted",\n' - ' "position": "end"\n' - ' }\n' - ' },\n' - ' "ops": [\n' - ' {"op":"replace_method","required":["className","methodName","replacement"],"optional":["returnType","parametersSignature","attributesContains"],"examples":[{"note":"match overload by signature","parametersSignature":"(int a, string b)"},{"note":"ensure attributes retained","attributesContains":"ContextMenu"}]},\n' - ' {"op":"insert_method","required":["className","replacement"],"position":{"enum":["start","end","after","before"],"after_requires":"afterMethodName","before_requires":"beforeMethodName"}},\n' - ' {"op":"delete_method","required":["className","methodName"]},\n' - ' {"op":"anchor_insert","required":["anchor","text"],"notes":"regex; position=before|after"}\n' - ' ],\n' - ' "apply_text_edits_recipe": {\n' - ' "step1_read": { "tool": "resources/read", "args": {"uri": "unity://path/Assets/Scripts/Interaction/SmartReach.cs"} },\n' - ' "step2_apply": {\n' - ' "tool": "manage_script",\n' - ' "args": {\n' - ' "action": "apply_text_edits",\n' - ' "name": "SmartReach", "path": "Assets/Scripts/Interaction",\n' - ' "edits": [{"startLine": 42, "startCol": 1, "endLine": 42, "endCol": 1, "newText": "[MyAttr]\\n"}],\n' - ' "precondition_sha256": "",\n' - ' "options": {"refresh": "immediate", "validate": "standard"}\n' - ' }\n' - ' },\n' - ' "note": "newText is for apply_text_edits ranges only; use replacement in script_apply_edits ops."\n' - ' },\n' - ' "examples": [\n' - ' {\n' - ' "title": "Replace a method",\n' - ' "args": {\n' - ' "name": "SmartReach",\n' - ' "path": "Assets/Scripts/Interaction",\n' - ' "edits": [\n' - ' {"op":"replace_method","className":"SmartReach","methodName":"HasTarget","replacement":"public bool HasTarget() { return currentTarget != null; }"}\n' - ' ],\n' - ' "options": { "validate": "standard", "refresh": "immediate" }\n' - ' }\n' - ' },\n' - ' {\n' - ' "title": "Insert a method after another",\n' - ' "args": {\n' - ' "name": "SmartReach",\n' - ' "path": "Assets/Scripts/Interaction",\n' - ' "edits": [\n' - ' {"op":"insert_method","className":"SmartReach","replacement":"public void PrintSeries() { Debug.Log(seriesName); }","position":"after","afterMethodName":"GetCurrentTarget"}\n' - ' ]\n' - ' }\n' - ' }\n' - ' ]\n' - '}\n' - ) - sha = hashlib.sha256(spec_json.encode("utf-8")).hexdigest() - return {"success": True, "data": {"text": spec_json, "metadata": {"sha256": sha}}} +@mcp_for_unity_tool(description=("Reads a resource by unity://path/... URI with optional slicing.")) +async def read_resource( + ctx: Context, + uri: Annotated[str, "The resource URI to read under Assets/"], + start_line: Annotated[int, + "The starting line number (0-based)"] | None = None, + line_count: Annotated[int, + "The number of lines to read"] | None = None, + head_bytes: Annotated[int, + "The number of bytes to read from the start of the file"] | None = None, + tail_lines: Annotated[int, + "The number of lines to read from the end of the file"] | None = None, + project_root: Annotated[str, + "The project root directory"] | None = None, + request: Annotated[str, "The request ID"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing read_resource: {uri}") + try: + # Serve the canonical spec directly when requested (allow bare or with scheme) + if uri in ("unity://spec/script-edits", "spec/script-edits", "script-edits"): + spec_json = ( + '{\n' + ' "name": "Unity MCP - Script Edits v1",\n' + ' "target_tool": "script_apply_edits",\n' + ' "canonical_rules": {\n' + ' "always_use": ["op","className","methodName","replacement","afterMethodName","beforeMethodName"],\n' + ' "never_use": ["new_method","anchor_method","content","newText"],\n' + ' "defaults": {\n' + ' "className": "\u2190 server will default to \'name\' when omitted",\n' + ' "position": "end"\n' + ' }\n' + ' },\n' + ' "ops": [\n' + ' {"op":"replace_method","required":["className","methodName","replacement"],"optional":["returnType","parametersSignature","attributesContains"],"examples":[{"note":"match overload by signature","parametersSignature":"(int a, string b)"},{"note":"ensure attributes retained","attributesContains":"ContextMenu"}]},\n' + ' {"op":"insert_method","required":["className","replacement"],"position":{"enum":["start","end","after","before"],"after_requires":"afterMethodName","before_requires":"beforeMethodName"}},\n' + ' {"op":"delete_method","required":["className","methodName"]},\n' + ' {"op":"anchor_insert","required":["anchor","text"],"notes":"regex; position=before|after"}\n' + ' ],\n' + ' "apply_text_edits_recipe": {\n' + ' "step1_read": { "tool": "resources/read", "args": {"uri": "unity://path/Assets/Scripts/Interaction/SmartReach.cs"} },\n' + ' "step2_apply": {\n' + ' "tool": "manage_script",\n' + ' "args": {\n' + ' "action": "apply_text_edits",\n' + ' "name": "SmartReach", "path": "Assets/Scripts/Interaction",\n' + ' "edits": [{"startLine": 42, "startCol": 1, "endLine": 42, "endCol": 1, "newText": "[MyAttr]\\n"}],\n' + ' "precondition_sha256": "",\n' + ' "options": {"refresh": "immediate", "validate": "standard"}\n' + ' }\n' + ' },\n' + ' "note": "newText is for apply_text_edits ranges only; use replacement in script_apply_edits ops."\n' + ' },\n' + ' "examples": [\n' + ' {\n' + ' "title": "Replace a method",\n' + ' "args": {\n' + ' "name": "SmartReach",\n' + ' "path": "Assets/Scripts/Interaction",\n' + ' "edits": [\n' + ' {"op":"replace_method","className":"SmartReach","methodName":"HasTarget","replacement":"public bool HasTarget() { return currentTarget != null; }"}\n' + ' ],\n' + ' "options": { "validate": "standard", "refresh": "immediate" }\n' + ' }\n' + ' },\n' + ' {\n' + ' "title": "Insert a method after another",\n' + ' "args": {\n' + ' "name": "SmartReach",\n' + ' "path": "Assets/Scripts/Interaction",\n' + ' "edits": [\n' + ' {"op":"insert_method","className":"SmartReach","replacement":"public void PrintSeries() { Debug.Log(seriesName); }","position":"after","afterMethodName":"GetCurrentTarget"}\n' + ' ]\n' + ' }\n' + ' }\n' + ' ]\n' + '}\n' + ) + sha = hashlib.sha256(spec_json.encode("utf-8")).hexdigest() + return {"success": True, "data": {"text": spec_json, "metadata": {"sha256": sha}}} - project = _resolve_project_root(project_root) - p = _resolve_safe_path_from_uri(uri, project) - if not p or not p.exists() or not p.is_file(): - return {"success": False, "error": f"Resource not found: {uri}"} - try: - p.relative_to(project / "Assets") - except ValueError: - return {"success": False, "error": "Read restricted to Assets/"} - # Natural-language convenience: request like "last 120 lines", "first 200 lines", - # "show 40 lines around MethodName", etc. - if request: - req = request.strip().lower() - m = re.search(r"last\s+(\d+)\s+lines", req) - if m: - tail_lines = int(m.group(1)) - m = re.search(r"first\s+(\d+)\s+lines", req) - if m: - start_line = 1 - line_count = int(m.group(1)) - m = re.search(r"first\s+(\d+)\s*bytes", req) - if m: - head_bytes = int(m.group(1)) - m = re.search( - r"show\s+(\d+)\s+lines\s+around\s+([A-Za-z_][A-Za-z0-9_]*)", req) - if m: - window = int(m.group(1)) - method = m.group(2) - # naive search for method header to get a line number - text_all = p.read_text(encoding="utf-8") - lines_all = text_all.splitlines() - pat = re.compile( - rf"^\s*(?:\[[^\]]+\]\s*)*(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial).*?\b{re.escape(method)}\s*\(", re.MULTILINE) - hit_line = None - for i, line in enumerate(lines_all, start=1): - if pat.search(line): - hit_line = i - break - if hit_line: - half = max(1, window // 2) - start_line = max(1, hit_line - half) - line_count = window + project = _resolve_project_root(project_root) + p = _resolve_safe_path_from_uri(uri, project) + if not p or not p.exists() or not p.is_file(): + return {"success": False, "error": f"Resource not found: {uri}"} + try: + p.relative_to(project / "Assets") + except ValueError: + return {"success": False, "error": "Read restricted to Assets/"} + # Natural-language convenience: request like "last 120 lines", "first 200 lines", + # "show 40 lines around MethodName", etc. + if request: + req = request.strip().lower() + m = re.search(r"last\s+(\d+)\s+lines", req) + if m: + tail_lines = int(m.group(1)) + m = re.search(r"first\s+(\d+)\s+lines", req) + if m: + start_line = 1 + line_count = int(m.group(1)) + m = re.search(r"first\s+(\d+)\s*bytes", req) + if m: + head_bytes = int(m.group(1)) + m = re.search( + r"show\s+(\d+)\s+lines\s+around\s+([A-Za-z_][A-Za-z0-9_]*)", req) + if m: + window = int(m.group(1)) + method = m.group(2) + # naive search for method header to get a line number + text_all = p.read_text(encoding="utf-8") + lines_all = text_all.splitlines() + pat = re.compile( + rf"^\s*(?:\[[^\]]+\]\s*)*(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial).*?\b{re.escape(method)}\s*\(", re.MULTILINE) + hit_line = None + for i, line in enumerate(lines_all, start=1): + if pat.search(line): + hit_line = i + break + if hit_line: + half = max(1, window // 2) + start_line = max(1, hit_line - half) + line_count = window - # Coerce numeric inputs defensively (string/float -> int) - start_line = _coerce_int(start_line) - line_count = _coerce_int(line_count) - head_bytes = _coerce_int(head_bytes, minimum=1) - tail_lines = _coerce_int(tail_lines, minimum=1) + # Coerce numeric inputs defensively (string/float -> int) + start_line = _coerce_int(start_line) + line_count = _coerce_int(line_count) + head_bytes = _coerce_int(head_bytes, minimum=1) + tail_lines = _coerce_int(tail_lines, minimum=1) - # Compute SHA over full file contents (metadata-only default) - full_bytes = p.read_bytes() - full_sha = hashlib.sha256(full_bytes).hexdigest() + # Compute SHA over full file contents (metadata-only default) + full_bytes = p.read_bytes() + full_sha = hashlib.sha256(full_bytes).hexdigest() - # Selection only when explicitly requested via windowing args or request text hints - selection_requested = bool(head_bytes or tail_lines or ( - start_line is not None and line_count is not None) or request) - if selection_requested: - # Mutually exclusive windowing options precedence: - # 1) head_bytes, 2) tail_lines, 3) start_line+line_count, else full text - if head_bytes and head_bytes > 0: - raw = full_bytes[: head_bytes] - text = raw.decode("utf-8", errors="replace") - else: - text = full_bytes.decode("utf-8", errors="replace") - if tail_lines is not None and tail_lines > 0: - lines = text.splitlines() - n = max(0, tail_lines) - text = "\n".join(lines[-n:]) - elif start_line is not None and line_count is not None and line_count >= 0: - lines = text.splitlines() - s = max(0, start_line - 1) - e = min(len(lines), s + line_count) - text = "\n".join(lines[s:e]) - return {"success": True, "data": {"text": text, "metadata": {"sha256": full_sha, "lengthBytes": len(full_bytes)}}} + # Selection only when explicitly requested via windowing args or request text hints + selection_requested = bool(head_bytes or tail_lines or ( + start_line is not None and line_count is not None) or request) + if selection_requested: + # Mutually exclusive windowing options precedence: + # 1) head_bytes, 2) tail_lines, 3) start_line+line_count, else full text + if head_bytes and head_bytes > 0: + raw = full_bytes[: head_bytes] + text = raw.decode("utf-8", errors="replace") else: - # Default: metadata only - return {"success": True, "data": {"metadata": {"sha256": full_sha, "lengthBytes": len(full_bytes)}}} - except Exception as e: - return {"success": False, "error": str(e)} + text = full_bytes.decode("utf-8", errors="replace") + if tail_lines is not None and tail_lines > 0: + lines = text.splitlines() + n = max(0, tail_lines) + text = "\n".join(lines[-n:]) + elif start_line is not None and line_count is not None and line_count >= 0: + lines = text.splitlines() + s = max(0, start_line - 1) + e = min(len(lines), s + line_count) + text = "\n".join(lines[s:e]) + return {"success": True, "data": {"text": text, "metadata": {"sha256": full_sha, "lengthBytes": len(full_bytes)}}} + else: + # Default: metadata only + return {"success": True, "data": {"metadata": {"sha256": full_sha, "lengthBytes": len(full_bytes)}}} + except Exception as e: + return {"success": False, "error": str(e)} - @mcp.tool(name="find_in_file", description="Searches a file with a regex pattern and returns line numbers and excerpts.") - @telemetry_tool("find_in_file") - async def find_in_file( - ctx: Context, - uri: Annotated[str, "The resource URI to search under Assets/ or file path form supported by read_resource"], - pattern: Annotated[str, "The regex pattern to search for"], - ignore_case: Annotated[bool, "Case-insensitive search"] | None = True, - project_root: Annotated[str, - "The project root directory"] | None = None, - max_results: Annotated[int, - "Cap results to avoid huge payloads"] = 200, - ) -> dict[str, Any]: - ctx.info(f"Processing find_in_file: {uri}") - try: - project = _resolve_project_root(project_root) - p = _resolve_safe_path_from_uri(uri, project) - if not p or not p.exists() or not p.is_file(): - return {"success": False, "error": f"Resource not found: {uri}"} - text = p.read_text(encoding="utf-8") - flags = re.MULTILINE - if ignore_case: - flags |= re.IGNORECASE - rx = re.compile(pattern, flags) +@mcp_for_unity_tool(description="Searches a file with a regex pattern and returns line numbers and excerpts.") +async def find_in_file( + ctx: Context, + uri: Annotated[str, "The resource URI to search under Assets/ or file path form supported by read_resource"], + pattern: Annotated[str, "The regex pattern to search for"], + ignore_case: Annotated[bool, "Case-insensitive search"] | None = True, + project_root: Annotated[str, + "The project root directory"] | None = None, + max_results: Annotated[int, + "Cap results to avoid huge payloads"] = 200, +) -> dict[str, Any]: + ctx.info(f"Processing find_in_file: {uri}") + try: + project = _resolve_project_root(project_root) + p = _resolve_safe_path_from_uri(uri, project) + if not p or not p.exists() or not p.is_file(): + return {"success": False, "error": f"Resource not found: {uri}"} - results = [] - max_results_int = _coerce_int(max_results, default=200, minimum=1) - lines = text.splitlines() - for i, line in enumerate(lines, start=1): - m = rx.search(line) - if m: - start_col = m.start() + 1 # 1-based - end_col = m.end() + 1 # 1-based, end exclusive - results.append({ - "startLine": i, - "startCol": start_col, - "endLine": i, - "endCol": end_col, - }) - if max_results_int and len(results) >= max_results_int: - break + text = p.read_text(encoding="utf-8") + flags = re.MULTILINE + if ignore_case: + flags |= re.IGNORECASE + rx = re.compile(pattern, flags) + + results = [] + max_results_int = _coerce_int(max_results, default=200, minimum=1) + lines = text.splitlines() + for i, line in enumerate(lines, start=1): + m = rx.search(line) + if m: + start_col = m.start() + 1 # 1-based + end_col = m.end() + 1 # 1-based, end exclusive + results.append({ + "startLine": i, + "startCol": start_col, + "endLine": i, + "endCol": end_col, + }) + if max_results_int and len(results) >= max_results_int: + break - return {"success": True, "data": {"matches": results, "count": len(results)}} - except Exception as e: - return {"success": False, "error": str(e)} + return {"success": True, "data": {"matches": results, "count": len(results)}} + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/script_apply_edits.py b/UnityMcpBridge/UnityMcpServer~/src/tools/script_apply_edits.py new file mode 100644 index 00000000..59fbbc61 --- /dev/null +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/script_apply_edits.py @@ -0,0 +1,966 @@ +import base64 +import hashlib +import re +from typing import Annotated, Any + +from mcp.server.fastmcp import Context + +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +def _apply_edits_locally(original_text: str, edits: list[dict[str, Any]]) -> str: + text = original_text + for edit in edits or []: + op = ( + (edit.get("op") + or edit.get("operation") + or edit.get("type") + or edit.get("mode") + or "") + .strip() + .lower() + ) + + if not op: + allowed = "anchor_insert, prepend, append, replace_range, regex_replace" + raise RuntimeError( + f"op is required; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation)." + ) + + if op == "prepend": + prepend_text = edit.get("text", "") + text = (prepend_text if prepend_text.endswith( + "\n") else prepend_text + "\n") + text + elif op == "append": + append_text = edit.get("text", "") + if not text.endswith("\n"): + text += "\n" + text += append_text + if not text.endswith("\n"): + text += "\n" + elif op == "anchor_insert": + anchor = edit.get("anchor", "") + position = (edit.get("position") or "before").lower() + insert_text = edit.get("text", "") + flags = re.MULTILINE | ( + re.IGNORECASE if edit.get("ignore_case") else 0) + + # Find the best match using improved heuristics + match = _find_best_anchor_match( + anchor, text, flags, bool(edit.get("prefer_last", True))) + if not match: + if edit.get("allow_noop", True): + continue + raise RuntimeError(f"anchor not found: {anchor}") + idx = match.start() if position == "before" else match.end() + text = text[:idx] + insert_text + text[idx:] + elif op == "replace_range": + start_line = int(edit.get("startLine", 1)) + start_col = int(edit.get("startCol", 1)) + end_line = int(edit.get("endLine", start_line)) + end_col = int(edit.get("endCol", 1)) + replacement = edit.get("text", "") + lines = text.splitlines(keepends=True) + max_line = len(lines) + 1 # 1-based, exclusive end + if (start_line < 1 or end_line < start_line or end_line > max_line + or start_col < 1 or end_col < 1): + raise RuntimeError("replace_range out of bounds") + + def index_of(line: int, col: int) -> int: + if line <= len(lines): + return sum(len(l) for l in lines[: line - 1]) + (col - 1) + return sum(len(l) for l in lines) + a = index_of(start_line, start_col) + b = index_of(end_line, end_col) + text = text[:a] + replacement + text[b:] + elif op == "regex_replace": + pattern = edit.get("pattern", "") + repl = edit.get("replacement", "") + # Translate $n backrefs (our input) to Python \g + repl_py = re.sub(r"\$(\d+)", r"\\g<\1>", repl) + count = int(edit.get("count", 0)) # 0 = replace all + flags = re.MULTILINE + if edit.get("ignore_case"): + flags |= re.IGNORECASE + text = re.sub(pattern, repl_py, text, count=count, flags=flags) + else: + allowed = "anchor_insert, prepend, append, replace_range, regex_replace" + raise RuntimeError( + f"unknown edit op: {op}; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation).") + return text + + +def _find_best_anchor_match(pattern: str, text: str, flags: int, prefer_last: bool = True): + """ + Find the best anchor match using improved heuristics. + + For patterns like \\s*}\\s*$ that are meant to find class-ending braces, + this function uses heuristics to choose the most semantically appropriate match: + + 1. If prefer_last=True, prefer the last match (common for class-end insertions) + 2. Use indentation levels to distinguish class vs method braces + 3. Consider context to avoid matches inside strings/comments + + Args: + pattern: Regex pattern to search for + text: Text to search in + flags: Regex flags + prefer_last: If True, prefer the last match over the first + + Returns: + Match object of the best match, or None if no match found + """ + + # Find all matches + matches = list(re.finditer(pattern, text, flags)) + if not matches: + return None + + # If only one match, return it + if len(matches) == 1: + return matches[0] + + # For patterns that look like they're trying to match closing braces at end of lines + is_closing_brace_pattern = '}' in pattern and ( + '$' in pattern or pattern.endswith(r'\s*')) + + if is_closing_brace_pattern and prefer_last: + # Use heuristics to find the best closing brace match + return _find_best_closing_brace_match(matches, text) + + # Default behavior: use last match if prefer_last, otherwise first match + return matches[-1] if prefer_last else matches[0] + + +def _find_best_closing_brace_match(matches, text: str): + """ + Find the best closing brace match using C# structure heuristics. + + Enhanced heuristics for scope-aware matching: + 1. Prefer matches with lower indentation (likely class-level) + 2. Prefer matches closer to end of file + 3. Avoid matches that seem to be inside method bodies + 4. For #endregion patterns, ensure class-level context + 5. Validate insertion point is at appropriate scope + + Args: + matches: List of regex match objects + text: The full text being searched + + Returns: + The best match object + """ + if not matches: + return None + + scored_matches = [] + lines = text.splitlines() + + for match in matches: + score = 0 + start_pos = match.start() + + # Find which line this match is on + lines_before = text[:start_pos].count('\n') + line_num = lines_before + + if line_num < len(lines): + line_content = lines[line_num] + + # Calculate indentation level (lower is better for class braces) + indentation = len(line_content) - len(line_content.lstrip()) + + # Prefer lower indentation (class braces are typically less indented than method braces) + # Max 20 points for indentation=0 + score += max(0, 20 - indentation) + + # Prefer matches closer to end of file (class closing braces are typically at the end) + distance_from_end = len(lines) - line_num + # More points for being closer to end + score += max(0, 10 - distance_from_end) + + # Look at surrounding context to avoid method braces + context_start = max(0, line_num - 3) + context_end = min(len(lines), line_num + 2) + context_lines = lines[context_start:context_end] + + # Penalize if this looks like it's inside a method (has method-like patterns above) + for context_line in context_lines: + if re.search(r'\b(void|public|private|protected)\s+\w+\s*\(', context_line): + score -= 5 # Penalty for being near method signatures + + # Bonus if this looks like a class-ending brace (very minimal indentation and near EOF) + if indentation <= 4 and distance_from_end <= 3: + score += 15 # Bonus for likely class-ending brace + + scored_matches.append((score, match)) + + # Return the match with the highest score + scored_matches.sort(key=lambda x: x[0], reverse=True) + best_match = scored_matches[0][1] + + return best_match + + +def _infer_class_name(script_name: str) -> str: + # Default to script name as class name (common Unity pattern) + return (script_name or "").strip() + + +def _extract_code_after(keyword: str, request: str) -> str: + # Deprecated with NL removal; retained as no-op for compatibility + idx = request.lower().find(keyword) + if idx >= 0: + return request[idx + len(keyword):].strip() + return "" +# Removed _is_structurally_balanced - validation now handled by C# side using Unity's compiler services + + +def _normalize_script_locator(name: str, path: str) -> tuple[str, str]: + """Best-effort normalization of script "name" and "path". + + Accepts any of: + - name = "SmartReach", path = "Assets/Scripts/Interaction" + - name = "SmartReach.cs", path = "Assets/Scripts/Interaction" + - name = "Assets/Scripts/Interaction/SmartReach.cs", path = "" + - path = "Assets/Scripts/Interaction/SmartReach.cs" (name empty) + - name or path using uri prefixes: unity://path/..., file://... + - accidental duplicates like "Assets/.../SmartReach.cs/SmartReach.cs" + + Returns (name_without_extension, directory_path_under_Assets). + """ + n = (name or "").strip() + p = (path or "").strip() + + def strip_prefix(s: str) -> str: + if s.startswith("unity://path/"): + return s[len("unity://path/"):] + if s.startswith("file://"): + return s[len("file://"):] + return s + + def collapse_duplicate_tail(s: str) -> str: + # Collapse trailing "/X.cs/X.cs" to "/X.cs" + parts = s.split("/") + if len(parts) >= 2 and parts[-1] == parts[-2]: + parts = parts[:-1] + return "/".join(parts) + + # Prefer a full path if provided in either field + candidate = "" + for v in (n, p): + v2 = strip_prefix(v) + if v2.endswith(".cs") or v2.startswith("Assets/"): + candidate = v2 + break + + if candidate: + candidate = collapse_duplicate_tail(candidate) + # If a directory was passed in path and file in name, join them + if not candidate.endswith(".cs") and n.endswith(".cs"): + v2 = strip_prefix(n) + candidate = (candidate.rstrip("/") + "/" + v2.split("/")[-1]) + if candidate.endswith(".cs"): + parts = candidate.split("/") + file_name = parts[-1] + dir_path = "/".join(parts[:-1]) if len(parts) > 1 else "Assets" + base = file_name[:- + 3] if file_name.lower().endswith(".cs") else file_name + return base, dir_path + + # Fall back: remove extension from name if present and return given path + base_name = n[:-3] if n.lower().endswith(".cs") else n + return base_name, (p or "Assets") + + +def _with_norm(resp: dict[str, Any] | Any, edits: list[dict[str, Any]], routing: str | None = None) -> dict[str, Any] | Any: + if not isinstance(resp, dict): + return resp + data = resp.setdefault("data", {}) + data.setdefault("normalizedEdits", edits) + if routing: + data["routing"] = routing + return resp + + +def _err(code: str, message: str, *, expected: dict[str, Any] | None = None, rewrite: dict[str, Any] | None = None, + normalized: list[dict[str, Any]] | None = None, routing: str | None = None, extra: dict[str, Any] | None = None) -> dict[str, Any]: + payload: dict[str, Any] = {"success": False, + "code": code, "message": message} + data: dict[str, Any] = {} + if expected: + data["expected"] = expected + if rewrite: + data["rewrite_suggestion"] = rewrite + if normalized is not None: + data["normalizedEdits"] = normalized + if routing: + data["routing"] = routing + if extra: + data.update(extra) + if data: + payload["data"] = data + return payload + +# Natural-language parsing removed; clients should send structured edits. + + +@mcp_for_unity_tool(name="script_apply_edits", description=( + """Structured C# edits (methods/classes) with safer boundaries - prefer this over raw text. + Best practices: + - Prefer anchor_* ops for pattern-based insert/replace near stable markers + - Use replace_method/delete_method for whole-method changes (keeps signatures balanced) + - Avoid whole-file regex deletes; validators will guard unbalanced braces + - For tail insertions, prefer anchor/regex_replace on final brace (class closing) + - Pass options.validate='standard' for structural checks; 'relaxed' for interior-only edits + Canonical fields (use these exact keys): + - op: replace_method | insert_method | delete_method | anchor_insert | anchor_delete | anchor_replace + - className: string (defaults to 'name' if omitted on method/class ops) + - methodName: string (required for replace_method, delete_method) + - replacement: string (required for replace_method, insert_method) + - position: start | end | after | before (insert_method only) + - afterMethodName / beforeMethodName: string (required when position='after'/'before') + - anchor: regex string (for anchor_* ops) + - text: string (for anchor_insert/anchor_replace) + Examples: + 1) Replace a method: + { + "name": "SmartReach", + "path": "Assets/Scripts/Interaction", + "edits": [ + { + "op": "replace_method", + "className": "SmartReach", + "methodName": "HasTarget", + "replacement": "public bool HasTarget(){ return currentTarget!=null; }" + } + ], + "options": {"validate": "standard", "refresh": "immediate"} + } + "2) Insert a method after another: + { + "name": "SmartReach", + "path": "Assets/Scripts/Interaction", + "edits": [ + { + "op": "insert_method", + "className": "SmartReach", + "replacement": "public void PrintSeries(){ Debug.Log(seriesName); }", + "position": "after", + "afterMethodName": "GetCurrentTarget" + } + ], + } + ]""" +)) +def script_apply_edits( + ctx: Context, + name: Annotated[str, "Name of the script to edit"], + path: Annotated[str, "Path to the script to edit under Assets/ directory"], + edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script"], + options: Annotated[dict[str, Any], + "Options for the script edit"] | None = None, + script_type: Annotated[str, + "Type of the script to edit"] = "MonoBehaviour", + namespace: Annotated[str, + "Namespace of the script to edit"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing script_apply_edits: {name}") + # Normalize locator first so downstream calls target the correct script file. + name, path = _normalize_script_locator(name, path) + # Normalize unsupported or aliased ops to known structured/text paths + + def _unwrap_and_alias(edit: dict[str, Any]) -> dict[str, Any]: + # Unwrap single-key wrappers like {"replace_method": {...}} + for wrapper_key in ( + "replace_method", "insert_method", "delete_method", + "replace_class", "delete_class", + "anchor_insert", "anchor_replace", "anchor_delete", + ): + if wrapper_key in edit and isinstance(edit[wrapper_key], dict): + inner = dict(edit[wrapper_key]) + inner["op"] = wrapper_key + edit = inner + break + + e = dict(edit) + op = (e.get("op") or e.get("operation") or e.get( + "type") or e.get("mode") or "").strip().lower() + if op: + e["op"] = op + + # Common field aliases + if "class_name" in e and "className" not in e: + e["className"] = e.pop("class_name") + if "class" in e and "className" not in e: + e["className"] = e.pop("class") + if "method_name" in e and "methodName" not in e: + e["methodName"] = e.pop("method_name") + # Some clients use a generic 'target' for method name + if "target" in e and "methodName" not in e: + e["methodName"] = e.pop("target") + if "method" in e and "methodName" not in e: + e["methodName"] = e.pop("method") + if "new_content" in e and "replacement" not in e: + e["replacement"] = e.pop("new_content") + if "newMethod" in e and "replacement" not in e: + e["replacement"] = e.pop("newMethod") + if "new_method" in e and "replacement" not in e: + e["replacement"] = e.pop("new_method") + if "content" in e and "replacement" not in e: + e["replacement"] = e.pop("content") + if "after" in e and "afterMethodName" not in e: + e["afterMethodName"] = e.pop("after") + if "after_method" in e and "afterMethodName" not in e: + e["afterMethodName"] = e.pop("after_method") + if "before" in e and "beforeMethodName" not in e: + e["beforeMethodName"] = e.pop("before") + if "before_method" in e and "beforeMethodName" not in e: + e["beforeMethodName"] = e.pop("before_method") + # anchor_method → before/after based on position (default after) + if "anchor_method" in e: + anchor = e.pop("anchor_method") + pos = (e.get("position") or "after").strip().lower() + if pos == "before" and "beforeMethodName" not in e: + e["beforeMethodName"] = anchor + elif "afterMethodName" not in e: + e["afterMethodName"] = anchor + if "anchorText" in e and "anchor" not in e: + e["anchor"] = e.pop("anchorText") + if "pattern" in e and "anchor" not in e and e.get("op") and e["op"].startswith("anchor_"): + e["anchor"] = e.pop("pattern") + if "newText" in e and "text" not in e: + e["text"] = e.pop("newText") + + # CI compatibility (T‑A/T‑E): + # Accept method-anchored anchor_insert and upgrade to insert_method + # Example incoming shape: + # {"op":"anchor_insert","afterMethodName":"GetCurrentTarget","text":"..."} + if ( + e.get("op") == "anchor_insert" + and not e.get("anchor") + and (e.get("afterMethodName") or e.get("beforeMethodName")) + ): + e["op"] = "insert_method" + if "replacement" not in e: + e["replacement"] = e.get("text", "") + + # LSP-like range edit -> replace_range + if "range" in e and isinstance(e["range"], dict): + rng = e.pop("range") + start = rng.get("start", {}) + end = rng.get("end", {}) + # Convert 0-based to 1-based line/col + e["op"] = "replace_range" + e["startLine"] = int(start.get("line", 0)) + 1 + e["startCol"] = int(start.get("character", 0)) + 1 + e["endLine"] = int(end.get("line", 0)) + 1 + e["endCol"] = int(end.get("character", 0)) + 1 + if "newText" in edit and "text" not in e: + e["text"] = edit.get("newText", "") + return e + + normalized_edits: list[dict[str, Any]] = [] + for raw in edits or []: + e = _unwrap_and_alias(raw) + op = (e.get("op") or e.get("operation") or e.get( + "type") or e.get("mode") or "").strip().lower() + + # Default className to script name if missing on structured method/class ops + if op in ("replace_class", "delete_class", "replace_method", "delete_method", "insert_method") and not e.get("className"): + e["className"] = name + + # Map common aliases for text ops + if op in ("text_replace",): + e["op"] = "replace_range" + normalized_edits.append(e) + continue + if op in ("regex_delete",): + e["op"] = "regex_replace" + e.setdefault("text", "") + normalized_edits.append(e) + continue + if op == "regex_replace" and ("replacement" not in e): + if "text" in e: + e["replacement"] = e.get("text", "") + elif "insert" in e or "content" in e: + e["replacement"] = e.get( + "insert") or e.get("content") or "" + if op == "anchor_insert" and not (e.get("text") or e.get("insert") or e.get("content") or e.get("replacement")): + e["op"] = "anchor_delete" + normalized_edits.append(e) + continue + normalized_edits.append(e) + + edits = normalized_edits + normalized_for_echo = edits + + # Validate required fields and produce machine-parsable hints + def error_with_hint(message: str, expected: dict[str, Any], suggestion: dict[str, Any]) -> dict[str, Any]: + return _err("missing_field", message, expected=expected, rewrite=suggestion, normalized=normalized_for_echo) + + for e in edits or []: + op = e.get("op", "") + if op == "replace_method": + if not e.get("methodName"): + return error_with_hint( + "replace_method requires 'methodName'.", + {"op": "replace_method", "required": [ + "className", "methodName", "replacement"]}, + {"edits[0].methodName": "HasTarget"} + ) + if not (e.get("replacement") or e.get("text")): + return error_with_hint( + "replace_method requires 'replacement' (inline or base64).", + {"op": "replace_method", "required": [ + "className", "methodName", "replacement"]}, + {"edits[0].replacement": "public bool X(){ return true; }"} + ) + elif op == "insert_method": + if not (e.get("replacement") or e.get("text")): + return error_with_hint( + "insert_method requires a non-empty 'replacement'.", + {"op": "insert_method", "required": ["className", "replacement"], "position": { + "after_requires": "afterMethodName", "before_requires": "beforeMethodName"}}, + {"edits[0].replacement": "public void PrintSeries(){ Debug.Log(\"1,2,3\"); }"} + ) + pos = (e.get("position") or "").lower() + if pos == "after" and not e.get("afterMethodName"): + return error_with_hint( + "insert_method with position='after' requires 'afterMethodName'.", + {"op": "insert_method", "position": { + "after_requires": "afterMethodName"}}, + {"edits[0].afterMethodName": "GetCurrentTarget"} + ) + if pos == "before" and not e.get("beforeMethodName"): + return error_with_hint( + "insert_method with position='before' requires 'beforeMethodName'.", + {"op": "insert_method", "position": { + "before_requires": "beforeMethodName"}}, + {"edits[0].beforeMethodName": "GetCurrentTarget"} + ) + elif op == "delete_method": + if not e.get("methodName"): + return error_with_hint( + "delete_method requires 'methodName'.", + {"op": "delete_method", "required": [ + "className", "methodName"]}, + {"edits[0].methodName": "PrintSeries"} + ) + elif op in ("anchor_insert", "anchor_replace", "anchor_delete"): + if not e.get("anchor"): + return error_with_hint( + f"{op} requires 'anchor' (regex).", + {"op": op, "required": ["anchor"]}, + {"edits[0].anchor": "(?m)^\\s*public\\s+bool\\s+HasTarget\\s*\\("} + ) + if op in ("anchor_insert", "anchor_replace") and not (e.get("text") or e.get("replacement")): + return error_with_hint( + f"{op} requires 'text'.", + {"op": op, "required": ["anchor", "text"]}, + {"edits[0].text": "/* comment */\n"} + ) + + # Decide routing: structured vs text vs mixed + STRUCT = {"replace_class", "delete_class", "replace_method", "delete_method", + "insert_method", "anchor_delete", "anchor_replace", "anchor_insert"} + TEXT = {"prepend", "append", "replace_range", "regex_replace"} + ops_set = {(e.get("op") or "").lower() for e in edits or []} + all_struct = ops_set.issubset(STRUCT) + all_text = ops_set.issubset(TEXT) + mixed = not (all_struct or all_text) + + # If everything is structured (method/class/anchor ops), forward directly to Unity's structured editor. + if all_struct: + opts2 = dict(options or {}) + # For structured edits, prefer immediate refresh to avoid missed reloads when Editor is unfocused + opts2.setdefault("refresh", "immediate") + params_struct: dict[str, Any] = { + "action": "edit", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": edits, + "options": opts2, + } + resp_struct = send_command_with_retry( + "manage_script", params_struct) + if isinstance(resp_struct, dict) and resp_struct.get("success"): + pass # Optional sentinel reload removed (deprecated) + return _with_norm(resp_struct if isinstance(resp_struct, dict) else {"success": False, "message": str(resp_struct)}, normalized_for_echo, routing="structured") + + # 1) read from Unity + read_resp = send_command_with_retry("manage_script", { + "action": "read", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + }) + if not isinstance(read_resp, dict) or not read_resp.get("success"): + return read_resp if isinstance(read_resp, dict) else {"success": False, "message": str(read_resp)} + + data = read_resp.get("data") or read_resp.get( + "result", {}).get("data") or {} + contents = data.get("contents") + if contents is None and data.get("contentsEncoded") and data.get("encodedContents"): + contents = base64.b64decode( + data["encodedContents"]).decode("utf-8") + if contents is None: + return {"success": False, "message": "No contents returned from Unity read."} + + # Optional preview/dry-run: apply locally and return diff without writing + preview = bool((options or {}).get("preview")) + + # If we have a mixed batch (TEXT + STRUCT), apply text first with precondition, then structured + if mixed: + text_edits = [e for e in edits or [] if ( + e.get("op") or "").lower() in TEXT] + struct_edits = [e for e in edits or [] if ( + e.get("op") or "").lower() in STRUCT] + try: + base_text = contents + + def line_col_from_index(idx: int) -> tuple[int, int]: + line = base_text.count("\n", 0, idx) + 1 + last_nl = base_text.rfind("\n", 0, idx) + col = (idx - (last_nl + 1)) + \ + 1 if last_nl >= 0 else idx + 1 + return line, col + + at_edits: list[dict[str, Any]] = [] + for e in text_edits: + opx = (e.get("op") or e.get("operation") or e.get( + "type") or e.get("mode") or "").strip().lower() + text_field = e.get("text") or e.get("insert") or e.get( + "content") or e.get("replacement") or "" + if opx == "anchor_insert": + anchor = e.get("anchor") or "" + position = (e.get("position") or "after").lower() + flags = re.MULTILINE | ( + re.IGNORECASE if e.get("ignore_case") else 0) + try: + # Use improved anchor matching logic + m = _find_best_anchor_match( + anchor, base_text, flags, prefer_last=True) + except Exception as ex: + return _with_norm(_err("bad_regex", f"Invalid anchor regex: {ex}", normalized=normalized_for_echo, routing="mixed/text-first", extra={"hint": "Escape parentheses/braces or use a simpler anchor."}), normalized_for_echo, routing="mixed/text-first") + if not m: + return _with_norm({"success": False, "code": "anchor_not_found", "message": f"anchor not found: {anchor}"}, normalized_for_echo, routing="mixed/text-first") + idx = m.start() if position == "before" else m.end() + # Normalize insertion to avoid jammed methods + text_field_norm = text_field + if not text_field_norm.startswith("\n"): + text_field_norm = "\n" + text_field_norm + if not text_field_norm.endswith("\n"): + text_field_norm = text_field_norm + "\n" + sl, sc = line_col_from_index(idx) + at_edits.append( + {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": text_field_norm}) + # do not mutate base_text when building atomic spans + elif opx == "replace_range": + if all(k in e for k in ("startLine", "startCol", "endLine", "endCol")): + at_edits.append({ + "startLine": int(e.get("startLine", 1)), + "startCol": int(e.get("startCol", 1)), + "endLine": int(e.get("endLine", 1)), + "endCol": int(e.get("endCol", 1)), + "newText": text_field + }) + else: + return _with_norm(_err("missing_field", "replace_range requires startLine/startCol/endLine/endCol", normalized=normalized_for_echo, routing="mixed/text-first"), normalized_for_echo, routing="mixed/text-first") + elif opx == "regex_replace": + pattern = e.get("pattern") or "" + try: + regex_obj = re.compile(pattern, re.MULTILINE | ( + re.IGNORECASE if e.get("ignore_case") else 0)) + except Exception as ex: + return _with_norm(_err("bad_regex", f"Invalid regex pattern: {ex}", normalized=normalized_for_echo, routing="mixed/text-first", extra={"hint": "Escape special chars or prefer structured delete for methods."}), normalized_for_echo, routing="mixed/text-first") + m = regex_obj.search(base_text) + if not m: + continue + # Expand $1, $2... in replacement using this match + + def _expand_dollars(rep: str, _m=m) -> str: + return re.sub(r"\$(\d+)", lambda g: _m.group(int(g.group(1))) or "", rep) + repl = _expand_dollars(text_field) + sl, sc = line_col_from_index(m.start()) + el, ec = line_col_from_index(m.end()) + at_edits.append( + {"startLine": sl, "startCol": sc, "endLine": el, "endCol": ec, "newText": repl}) + # do not mutate base_text when building atomic spans + elif opx in ("prepend", "append"): + if opx == "prepend": + sl, sc = 1, 1 + at_edits.append( + {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": text_field}) + # prepend can be applied atomically without local mutation + else: + # Insert at true EOF position (handles both \n and \r\n correctly) + eof_idx = len(base_text) + sl, sc = line_col_from_index(eof_idx) + new_text = ("\n" if not base_text.endswith( + "\n") else "") + text_field + at_edits.append( + {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": new_text}) + # do not mutate base_text when building atomic spans + else: + return _with_norm(_err("unknown_op", f"Unsupported text edit op: {opx}", normalized=normalized_for_echo, routing="mixed/text-first"), normalized_for_echo, routing="mixed/text-first") + + sha = hashlib.sha256(base_text.encode("utf-8")).hexdigest() + if at_edits: + params_text: dict[str, Any] = { + "action": "apply_text_edits", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": at_edits, + "precondition_sha256": sha, + "options": {"refresh": (options or {}).get("refresh", "debounced"), "validate": (options or {}).get("validate", "standard"), "applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential"))} + } + resp_text = send_command_with_retry( + "manage_script", params_text) + if not (isinstance(resp_text, dict) and resp_text.get("success")): + return _with_norm(resp_text if isinstance(resp_text, dict) else {"success": False, "message": str(resp_text)}, normalized_for_echo, routing="mixed/text-first") + # Optional sentinel reload removed (deprecated) + except Exception as e: + return _with_norm({"success": False, "message": f"Text edit conversion failed: {e}"}, normalized_for_echo, routing="mixed/text-first") + + if struct_edits: + opts2 = dict(options or {}) + # Prefer debounced background refresh unless explicitly overridden + opts2.setdefault("refresh", "debounced") + params_struct: dict[str, Any] = { + "action": "edit", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": struct_edits, + "options": opts2 + } + resp_struct = send_command_with_retry( + "manage_script", params_struct) + if isinstance(resp_struct, dict) and resp_struct.get("success"): + pass # Optional sentinel reload removed (deprecated) + return _with_norm(resp_struct if isinstance(resp_struct, dict) else {"success": False, "message": str(resp_struct)}, normalized_for_echo, routing="mixed/text-first") + + return _with_norm({"success": True, "message": "Applied text edits (no structured ops)"}, normalized_for_echo, routing="mixed/text-first") + + # If the edits are text-ops, prefer sending them to Unity's apply_text_edits with precondition + # so header guards and validation run on the C# side. + # Supported conversions: anchor_insert, replace_range, regex_replace (first match only). + text_ops = {(e.get("op") or e.get("operation") or e.get("type") or e.get( + "mode") or "").strip().lower() for e in (edits or [])} + structured_kinds = {"replace_class", "delete_class", + "replace_method", "delete_method", "insert_method", "anchor_insert"} + if not text_ops.issubset(structured_kinds): + # Convert to apply_text_edits payload + try: + base_text = contents + + def line_col_from_index(idx: int) -> tuple[int, int]: + # 1-based line/col against base buffer + line = base_text.count("\n", 0, idx) + 1 + last_nl = base_text.rfind("\n", 0, idx) + col = (idx - (last_nl + 1)) + \ + 1 if last_nl >= 0 else idx + 1 + return line, col + + at_edits: list[dict[str, Any]] = [] + import re as _re + for e in edits or []: + op = (e.get("op") or e.get("operation") or e.get( + "type") or e.get("mode") or "").strip().lower() + # aliasing for text field + text_field = e.get("text") or e.get( + "insert") or e.get("content") or "" + if op == "anchor_insert": + anchor = e.get("anchor") or "" + position = (e.get("position") or "after").lower() + # Use improved anchor matching logic with helpful errors, honoring ignore_case + try: + flags = re.MULTILINE | ( + re.IGNORECASE if e.get("ignore_case") else 0) + m = _find_best_anchor_match( + anchor, base_text, flags, prefer_last=True) + except Exception as ex: + return _with_norm(_err("bad_regex", f"Invalid anchor regex: {ex}", normalized=normalized_for_echo, routing="text", extra={"hint": "Escape parentheses/braces or use a simpler anchor."}), normalized_for_echo, routing="text") + if not m: + return _with_norm({"success": False, "code": "anchor_not_found", "message": f"anchor not found: {anchor}"}, normalized_for_echo, routing="text") + idx = m.start() if position == "before" else m.end() + # Normalize insertion newlines + if text_field and not text_field.startswith("\n"): + text_field = "\n" + text_field + if text_field and not text_field.endswith("\n"): + text_field = text_field + "\n" + sl, sc = line_col_from_index(idx) + at_edits.append({ + "startLine": sl, + "startCol": sc, + "endLine": sl, + "endCol": sc, + "newText": text_field or "" + }) + # Do not mutate base buffer when building an atomic batch + elif op == "replace_range": + # Directly forward if already in line/col form + if "startLine" in e: + at_edits.append({ + "startLine": int(e.get("startLine", 1)), + "startCol": int(e.get("startCol", 1)), + "endLine": int(e.get("endLine", 1)), + "endCol": int(e.get("endCol", 1)), + "newText": text_field + }) + else: + # If only indices provided, skip (we don't support index-based here) + return _with_norm({"success": False, "code": "missing_field", "message": "replace_range requires startLine/startCol/endLine/endCol"}, normalized_for_echo, routing="text") + elif op == "regex_replace": + pattern = e.get("pattern") or "" + repl = text_field + flags = re.MULTILINE | ( + re.IGNORECASE if e.get("ignore_case") else 0) + # Early compile for clearer error messages + try: + regex_obj = re.compile(pattern, flags) + except Exception as ex: + return _with_norm(_err("bad_regex", f"Invalid regex pattern: {ex}", normalized=normalized_for_echo, routing="text", extra={"hint": "Escape special chars or prefer structured delete for methods."}), normalized_for_echo, routing="text") + # Use smart anchor matching for consistent behavior with anchor_insert + m = _find_best_anchor_match( + pattern, base_text, flags, prefer_last=True) + if not m: + continue + # Expand $1, $2... backrefs in replacement using the first match (consistent with mixed-path behavior) + + def _expand_dollars(rep: str, _m=m) -> str: + return re.sub(r"\$(\d+)", lambda g: _m.group(int(g.group(1))) or "", rep) + repl_expanded = _expand_dollars(repl) + # Let C# side handle validation using Unity's built-in compiler services + sl, sc = line_col_from_index(m.start()) + el, ec = line_col_from_index(m.end()) + at_edits.append({ + "startLine": sl, + "startCol": sc, + "endLine": el, + "endCol": ec, + "newText": repl_expanded + }) + # Do not mutate base buffer when building an atomic batch + else: + return _with_norm({"success": False, "code": "unsupported_op", "message": f"Unsupported text edit op for server-side apply_text_edits: {op}"}, normalized_for_echo, routing="text") + + if not at_edits: + return _with_norm({"success": False, "code": "no_spans", "message": "No applicable text edit spans computed (anchor not found or zero-length)."}, normalized_for_echo, routing="text") + + sha = hashlib.sha256(base_text.encode("utf-8")).hexdigest() + params: dict[str, Any] = { + "action": "apply_text_edits", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": at_edits, + "precondition_sha256": sha, + "options": { + "refresh": (options or {}).get("refresh", "debounced"), + "validate": (options or {}).get("validate", "standard"), + "applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential")) + } + } + resp = send_command_with_retry("manage_script", params) + if isinstance(resp, dict) and resp.get("success"): + pass # Optional sentinel reload removed (deprecated) + return _with_norm( + resp if isinstance(resp, dict) else { + "success": False, "message": str(resp)}, + normalized_for_echo, + routing="text" + ) + except Exception as e: + return _with_norm({"success": False, "code": "conversion_failed", "message": f"Edit conversion failed: {e}"}, normalized_for_echo, routing="text") + + # For regex_replace, honor preview consistently: if preview=true, always return diff without writing. + # If confirm=false (default) and preview not requested, return diff and instruct confirm=true to apply. + if "regex_replace" in text_ops and (preview or not (options or {}).get("confirm")): + try: + preview_text = _apply_edits_locally(contents, edits) + import difflib + diff = list(difflib.unified_diff(contents.splitlines( + ), preview_text.splitlines(), fromfile="before", tofile="after", n=2)) + if len(diff) > 800: + diff = diff[:800] + ["... (diff truncated) ..."] + if preview: + return {"success": True, "message": "Preview only (no write)", "data": {"diff": "\n".join(diff), "normalizedEdits": normalized_for_echo}} + return _with_norm({"success": False, "message": "Preview diff; set options.confirm=true to apply.", "data": {"diff": "\n".join(diff)}}, normalized_for_echo, routing="text") + except Exception as e: + return _with_norm({"success": False, "code": "preview_failed", "message": f"Preview failed: {e}"}, normalized_for_echo, routing="text") + # 2) apply edits locally (only if not text-ops) + try: + new_contents = _apply_edits_locally(contents, edits) + except Exception as e: + return {"success": False, "message": f"Edit application failed: {e}"} + + # Short-circuit no-op edits to avoid false "applied" reports downstream + if new_contents == contents: + return _with_norm({ + "success": True, + "message": "No-op: contents unchanged", + "data": {"no_op": True, "evidence": {"reason": "identical_content"}} + }, normalized_for_echo, routing="text") + + if preview: + # Produce a compact unified diff limited to small context + import difflib + a = contents.splitlines() + b = new_contents.splitlines() + diff = list(difflib.unified_diff( + a, b, fromfile="before", tofile="after", n=3)) + # Limit diff size to keep responses small + if len(diff) > 2000: + diff = diff[:2000] + ["... (diff truncated) ..."] + return {"success": True, "message": "Preview only (no write)", "data": {"diff": "\n".join(diff), "normalizedEdits": normalized_for_echo}} + + # 3) update to Unity + # Default refresh/validate for natural usage on text path as well + options = dict(options or {}) + options.setdefault("validate", "standard") + options.setdefault("refresh", "debounced") + + # Compute the SHA of the current file contents for the precondition + old_lines = contents.splitlines(keepends=True) + end_line = len(old_lines) + 1 # 1-based exclusive end + sha = hashlib.sha256(contents.encode("utf-8")).hexdigest() + + # Apply a whole-file text edit rather than the deprecated 'update' action + params = { + "action": "apply_text_edits", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": [ + { + "startLine": 1, + "startCol": 1, + "endLine": end_line, + "endCol": 1, + "newText": new_contents, + } + ], + "precondition_sha256": sha, + "options": options or {"validate": "standard", "refresh": "debounced"}, + } + + write_resp = send_command_with_retry("manage_script", params) + if isinstance(write_resp, dict) and write_resp.get("success"): + pass # Optional sentinel reload removed (deprecated) + return _with_norm( + write_resp if isinstance(write_resp, dict) + else {"success": False, "message": str(write_resp)}, + normalized_for_echo, + routing="text", + ) diff --git a/CursorHelp.md b/docs/CURSOR_HELP.md similarity index 100% rename from CursorHelp.md rename to docs/CURSOR_HELP.md diff --git a/docs/CUSTOM_TOOLS.md b/docs/CUSTOM_TOOLS.md new file mode 100644 index 00000000..1b988e1c --- /dev/null +++ b/docs/CUSTOM_TOOLS.md @@ -0,0 +1,287 @@ +# Adding Custom Tools to Unity MCP + +Unity MCP now supports auto-discovery of custom tools using decorators (Python) and attributes (C#). This allows you to easily extend the MCP server with your own tools without modifying core files. + +Be sure to review the developer README first: + +| [English](README-DEV.md) | [简体中文](README-DEV-zh.md) | +|---------------------------|------------------------------| + +## Python Side (MCP Server) + +### Creating a Custom Tool + +1. **Create a new Python file** in `UnityMcpBridge/UnityMcpServer~/src/tools/` (or any location that gets imported) + +2. **Use the `@mcp_for_unity_tool` decorator**: + +```python +from typing import Annotated, Any +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + +@mcp_for_unity_tool( + description="My custom tool that does something amazing" +) +def my_custom_tool( + ctx: Context, + param1: Annotated[str, "Description of param1"], + param2: Annotated[int, "Description of param2"] | None = None +) -> dict[str, Any]: + ctx.info(f"Processing my_custom_tool: {param1}") + + # Prepare parameters for Unity + params = { + "action": "do_something", + "param1": param1, + "param2": param2, + } + params = {k: v for k, v in params.items() if v is not None} + + # Send to Unity handler + response = send_command_with_retry("my_custom_tool", params) + return response if isinstance(response, dict) else {"success": False, "message": str(response)} +``` + +3. **The tool is automatically registered!** The decorator: + - Auto-generates the tool name from the function name (e.g., `my_custom_tool`) + - Registers the tool with FastMCP during module import + +4. **Rebuild the server** in the MCP for Unity window (in the Unity Editor) to apply the changes. + +### Decorator Options + +```python +@mcp_for_unity_tool( + name="custom_name", # Optional: the function name is used by default + description="Tool description", # Required: describe what the tool does +) +``` + +You can use all options available in FastMCP's `mcp.tool` function decorator: . + +**Note:** All tools should have the `description` field. It's not strictly required, however, that parameter is the best place to define a description so that most MCP clients can read it. See [issue #289](https://github.com/CoplayDev/unity-mcp/issues/289). + +### Auto-Discovery + +Tools are automatically discovered when: +- The Python file is in the `tools/` directory +- The file is imported during server startup +- The decorator `@mcp_for_unity_tool` is used + +## C# Side (Unity Editor) + +### Creating a Custom Tool Handler + +1. **Create a new C# file** anywhere in your Unity project (typically in `Editor/`) + +2. **Add the `[McpForUnityTool]` attribute** and implement `HandleCommand`: + +```csharp +using Newtonsoft.Json.Linq; +using MCPForUnity.Editor.Helpers; + +namespace MyProject.Editor.CustomTools +{ + // The name argument is optional, it uses a snake_case version of the class name by default + [McpForUnityTool("my_custom_tool")] + public static class MyCustomTool + { + public static object HandleCommand(JObject @params) + { + string action = @params["action"]?.ToString(); + string param1 = @params["param1"]?.ToString(); + int? param2 = @params["param2"]?.ToObject(); + + // Your custom logic here + if (string.IsNullOrEmpty(param1)) + { + return Response.Error("param1 is required"); + } + + // Do something amazing + DoSomethingAmazing(param1, param2); + + return Response.Success("Custom tool executed successfully!"); + } + + private static void DoSomethingAmazing(string param1, int? param2) + { + // Your implementation + } + } +} +``` + +3. **The tool is automatically registered!** Unity will discover it via reflection on startup. + +### Attribute Options + +```csharp +// Explicit command name +[McpForUnityTool("my_custom_tool")] +public static class MyCustomTool { } + +// Auto-generated from class name (MyCustomTool → my_custom_tool) +[McpForUnityTool] +public static class MyCustomTool { } +``` + +### Auto-Discovery + +Tools are automatically discovered when: +- The class has the `[McpForUnityTool]` attribute +- The class has a `public static HandleCommand(JObject)` method +- Unity loads the assembly containing the class + +## Complete Example: Custom Screenshot Tool + +### Python (`UnityMcpServer~/src/tools/screenshot_tool.py`) + +```python +from typing import Annotated, Any + +from mcp.server.fastmcp import Context + +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +@mcp_for_unity_tool( + description="Capture screenshots in Unity, saving them as PNGs" +) +def capture_screenshot( + ctx: Context, + filename: Annotated[str, "Screenshot filename without extension, e.g., screenshot_01"], +) -> dict[str, Any]: + ctx.info(f"Capturing screenshot: {filename}") + + params = { + "action": "capture", + "filename": filename, + } + params = {k: v for k, v in params.items() if v is not None} + + response = send_command_with_retry("capture_screenshot", params) + return response if isinstance(response, dict) else {"success": False, "message": str(response)} +``` + +### C# (`Editor/CaptureScreenshotTool.cs`) + +```csharp +using System.IO; +using Newtonsoft.Json.Linq; +using UnityEngine; +using MCPForUnity.Editor.Tools; + +namespace MyProject.Editor.Tools +{ + [McpForUnityTool("capture_screenshot")] + public static class CaptureScreenshotTool + { + public static object HandleCommand(JObject @params) + { + string filename = @params["filename"]?.ToString(); + + if (string.IsNullOrEmpty(filename)) + { + return MCPForUnity.Editor.Helpers.Response.Error("filename is required"); + } + + try + { + string absolutePath = Path.Combine(Application.dataPath, "Screenshots", filename); + Directory.CreateDirectory(Path.GetDirectoryName(absolutePath)); + + // Find the main camera + Camera camera = Camera.main; + if (camera == null) + { + camera = Object.FindFirstObjectByType(); + } + + if (camera == null) + { + return MCPForUnity.Editor.Helpers.Response.Error("No camera found in the scene"); + } + + // Create a RenderTexture + RenderTexture rt = new RenderTexture(Screen.width, Screen.height, 24); + camera.targetTexture = rt; + + // Render the camera's view + camera.Render(); + + // Read pixels from the RenderTexture + RenderTexture.active = rt; + Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false); + screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0); + screenshot.Apply(); + + // Clean up + camera.targetTexture = null; + RenderTexture.active = null; + Object.DestroyImmediate(rt); + + // Save to file + byte[] bytes = screenshot.EncodeToPNG(); + File.WriteAllBytes(absolutePath, bytes); + Object.DestroyImmediate(screenshot); + + return MCPForUnity.Editor.Helpers.Response.Success($"Screenshot saved to {absolutePath}", new + { + path = absolutePath, + }); + } + catch (System.Exception ex) + { + return MCPForUnity.Editor.Helpers.Response.Error($"Failed to capture screenshot: {ex.Message}"); + } + } + } +} +``` + +## Best Practices + +### Python +- ✅ Use type hints with `Annotated` for parameter documentation +- ✅ Return `dict[str, Any]` with `{"success": bool, "message": str, "data": Any}` +- ✅ Use `ctx.info()` for logging +- ✅ Handle errors gracefully and return structured error responses +- ✅ Use `send_command_with_retry()` for Unity communication + +### C# +- ✅ Use the `Response.Success()` and `Response.Error()` helper methods +- ✅ Validate input parameters before processing +- ✅ Use `@params["key"]?.ToObject()` for safe type conversion +- ✅ Return structured responses with meaningful data +- ✅ Handle exceptions and return error responses + +## Debugging + +### Python +- Check server logs: `~/Library/Application Support/UnityMCP/Logs/unity_mcp_server.log` +- Look for: `"Registered X MCP tools"` message on startup +- Use `ctx.info()` for debugging messages + +### C# +- Check Unity Console for: `"MCP-FOR-UNITY: Auto-discovered X tools"` message +- Look for warnings about missing `HandleCommand` methods +- Use `Debug.Log()` in your handler for debugging + +## Troubleshooting + +**Tool not appearing:** +- Python: Ensure the file is in `tools/` directory and imports the decorator +- C#: Ensure the class has `[McpForUnityTool]` attribute and `HandleCommand` method + +**Name conflicts:** +- Use explicit names in decorators/attributes to avoid conflicts +- Check registered tools: `CommandRegistry.GetAllCommandNames()` in C# + +**Tool not being called:** +- Verify the command name matches between Python and C# +- Check that parameters are being passed correctly +- Look for errors in logs diff --git a/README-DEV-zh.md b/docs/README-DEV-zh.md similarity index 100% rename from README-DEV-zh.md rename to docs/README-DEV-zh.md diff --git a/README-DEV.md b/docs/README-DEV.md similarity index 98% rename from README-DEV.md rename to docs/README-DEV.md index ddba6011..3bc63566 100644 --- a/README-DEV.md +++ b/docs/README-DEV.md @@ -21,7 +21,7 @@ Quick deployment and testing tools for MCP for Unity core changes. ## Switching MCP package sources quickly -Run this from the unity-mcp repo, not your game's roote directory. Use `mcp_source.py` to quickly switch between different MCP for Unity package sources: +Run this from the unity-mcp repo, not your game's root directory. Use `mcp_source.py` to quickly switch between different MCP for Unity package sources: **Usage:** ```bash diff --git a/TELEMETRY.md b/docs/TELEMETRY.md similarity index 100% rename from TELEMETRY.md rename to docs/TELEMETRY.md From ff736012fa7b578353c82d4ccf3c93fb8850c1a2 Mon Sep 17 00:00:00 2001 From: dsarno Date: Fri, 3 Oct 2025 17:08:39 -0700 Subject: [PATCH 4/7] Fix read_console includeStacktrace parameter behavior (#304) The includeStacktrace parameter was working backwards - when false, it would return the full message with embedded stack traces, and when true, it would extract the stack trace but the logic was inverted. Changes: - Always extract the first line as the message text - Only populate stackTrace field when includeStacktrace is true - Ensures clean, summary-only messages when includeStacktrace is false - Properly separates stack traces into their own field when requested This matches the expected Unity console behavior where the summary is shown by default, and stack traces are only shown when expanded. --- UnityMcpBridge/Editor/Tools/ReadConsole.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ReadConsole.cs b/UnityMcpBridge/Editor/Tools/ReadConsole.cs index 8a0147e3..e94e5d51 100644 --- a/UnityMcpBridge/Editor/Tools/ReadConsole.cs +++ b/UnityMcpBridge/Editor/Tools/ReadConsole.cs @@ -304,14 +304,18 @@ bool includeStacktrace // --- Formatting --- string stackTrace = includeStacktrace ? ExtractStackTrace(message) : null; - // Get first line if stack is present and requested, otherwise use full message - string messageOnly = - (includeStacktrace && !string.IsNullOrEmpty(stackTrace)) - ? message.Split( - new[] { '\n', '\r' }, - StringSplitOptions.RemoveEmptyEntries - )[0] - : message; + // Always get first line for the message, use full message only if no stack trace exists + string[] messageLines = message.Split( + new[] { '\n', '\r' }, + StringSplitOptions.RemoveEmptyEntries + ); + string messageOnly = messageLines.Length > 0 ? messageLines[0] : message; + + // If not including stacktrace, ensure we only show the first line + if (!includeStacktrace) + { + stackTrace = null; + } object formattedEntry = null; switch (format) From e9b1ae44c5bb5bfc88685b9a1b9af5174c71b0e1 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 3 Oct 2025 20:23:28 -0400 Subject: [PATCH 5/7] Rename plugin folder to MCPForUnity (#303) * Copy UnityMcpBridge into a new MCPForUnity folder This is to close #284 * refactor: rename UnityMcpBridge directory to MCPForUnity in docs * chore: rename UnityMcpBridge directory to MCPForUnity across workflow files * chore: rename UnityMcpBridge directory to MCPForUnity across all files * refactor: update import paths from UnityMcpBridge to MCPForUnity across test files * fix: update module import paths to use MCPForUnity instead of UnityMcpBridge * chore: update unity-mcp package path to MCPForUnity directory * feat: add OneTimeSetUp to initialize CommandRegistry before tests run Hopefully fix the CI failures * Apply recent fix to new folder * Temporarily trigger tests to see if CI works * Revert "Temporarily trigger tests to see if CI works" It works! This reverts commit 8c6eaaad07545cef047769f2c52fe506545a8161. --- .github/workflows/bump-version.yml | 18 +- .github/workflows/claude-nl-suite.yml | 18 +- .github/workflows/unity-tests.yml | 2 +- .gitignore | 1 - MCPForUnity/Editor.meta | 8 + MCPForUnity/Editor/AssemblyInfo.cs | 3 + MCPForUnity/Editor/AssemblyInfo.cs.meta | 11 + MCPForUnity/Editor/Data.meta | 8 + .../Editor/Data/DefaultServerConfig.cs | 17 + .../Editor/Data/DefaultServerConfig.cs.meta | 11 + MCPForUnity/Editor/Data/McpClients.cs | 198 ++ MCPForUnity/Editor/Data/McpClients.cs.meta | 11 + MCPForUnity/Editor/Dependencies.meta | 8 + .../Editor/Dependencies/DependencyManager.cs | 147 + .../Dependencies/DependencyManager.cs.meta | 11 + MCPForUnity/Editor/Dependencies/Models.meta | 8 + .../Models/DependencyCheckResult.cs | 96 + .../Models/DependencyCheckResult.cs.meta | 11 + .../Dependencies/Models/DependencyStatus.cs | 65 + .../Models/DependencyStatus.cs.meta | 11 + .../Dependencies/PlatformDetectors.meta | 8 + .../PlatformDetectors/IPlatformDetector.cs | 50 + .../IPlatformDetector.cs.meta | 11 + .../LinuxPlatformDetector.cs | 212 ++ .../LinuxPlatformDetector.cs.meta | 11 + .../MacOSPlatformDetector.cs | 212 ++ .../MacOSPlatformDetector.cs.meta | 11 + .../PlatformDetectors/PlatformDetectorBase.cs | 161 + .../PlatformDetectorBase.cs.meta | 11 + .../WindowsPlatformDetector.cs | 191 ++ .../WindowsPlatformDetector.cs.meta | 11 + MCPForUnity/Editor/External.meta | 8 + MCPForUnity/Editor/External/Tommy.cs | 2138 +++++++++++++ MCPForUnity/Editor/External/Tommy.cs.meta | 11 + MCPForUnity/Editor/Helpers.meta | 8 + .../Editor/Helpers/AssetPathUtility.cs | 29 + .../Editor/Helpers/AssetPathUtility.cs.meta | 11 + .../Editor/Helpers/CodexConfigHelper.cs | 243 ++ .../Editor/Helpers/CodexConfigHelper.cs.meta | 11 + .../Editor/Helpers/ConfigJsonBuilder.cs | 129 + .../Editor/Helpers/ConfigJsonBuilder.cs.meta | 11 + MCPForUnity/Editor/Helpers/ExecPath.cs | 278 ++ MCPForUnity/Editor/Helpers/ExecPath.cs.meta | 11 + .../Editor/Helpers/GameObjectSerializer.cs | 528 ++++ .../Helpers/GameObjectSerializer.cs.meta | 11 + .../Editor/Helpers/McpConfigFileHelper.cs | 186 ++ .../Helpers/McpConfigFileHelper.cs.meta | 11 + .../Editor/Helpers/McpConfigurationHelper.cs | 297 ++ .../Helpers/McpConfigurationHelper.cs.meta | 11 + MCPForUnity/Editor/Helpers/McpLog.cs | 31 + MCPForUnity/Editor/Helpers/McpLog.cs.meta | 13 + MCPForUnity/Editor/Helpers/McpPathResolver.cs | 123 + .../Editor/Helpers/McpPathResolver.cs.meta | 11 + MCPForUnity/Editor/Helpers/PackageDetector.cs | 107 + .../Editor/Helpers/PackageDetector.cs.meta | 11 + .../Editor/Helpers/PackageInstaller.cs | 43 + .../Editor/Helpers/PackageInstaller.cs.meta | 11 + MCPForUnity/Editor/Helpers/PortManager.cs | 319 ++ .../Editor/Helpers/PortManager.cs.meta | 11 + MCPForUnity/Editor/Helpers/Response.cs | 62 + MCPForUnity/Editor/Helpers/Response.cs.meta | 11 + MCPForUnity/Editor/Helpers/ServerInstaller.cs | 700 +++++ .../Editor/Helpers/ServerInstaller.cs.meta | 11 + .../Editor/Helpers/ServerPathResolver.cs | 141 + .../Editor/Helpers/ServerPathResolver.cs.meta | 11 + MCPForUnity/Editor/Helpers/TelemetryHelper.cs | 224 ++ .../Editor/Helpers/TelemetryHelper.cs.meta | 11 + MCPForUnity/Editor/Helpers/Vector3Helper.cs | 24 + .../Editor/Helpers/Vector3Helper.cs.meta | 11 + MCPForUnity/Editor/MCPForUnity.Editor.asmdef | 19 + .../Editor/MCPForUnity.Editor.asmdef.meta | 7 + MCPForUnity/Editor/MCPForUnityBridge.cs | 1194 ++++++++ MCPForUnity/Editor/MCPForUnityBridge.cs.meta | 11 + MCPForUnity/Editor/Models.meta | 8 + MCPForUnity/Editor/Models/Command.cs | 21 + MCPForUnity/Editor/Models/Command.cs.meta | 11 + MCPForUnity/Editor/Models/MCPConfigServer.cs | 19 + .../Editor/Models/MCPConfigServer.cs.meta | 11 + MCPForUnity/Editor/Models/MCPConfigServers.cs | 12 + .../Editor/Models/MCPConfigServers.cs.meta | 11 + MCPForUnity/Editor/Models/McpClient.cs | 47 + MCPForUnity/Editor/Models/McpClient.cs.meta | 11 + MCPForUnity/Editor/Models/McpConfig.cs | 12 + MCPForUnity/Editor/Models/McpConfig.cs.meta | 11 + MCPForUnity/Editor/Models/McpStatus.cs | 18 + MCPForUnity/Editor/Models/McpStatus.cs.meta | 11 + MCPForUnity/Editor/Models/McpTypes.cs | 13 + MCPForUnity/Editor/Models/McpTypes.cs.meta | 11 + MCPForUnity/Editor/Models/ServerConfig.cs | 36 + .../Editor/Models/ServerConfig.cs.meta | 11 + MCPForUnity/Editor/Setup.meta | 8 + MCPForUnity/Editor/Setup/SetupWizard.cs | 150 + MCPForUnity/Editor/Setup/SetupWizard.cs.meta | 11 + MCPForUnity/Editor/Setup/SetupWizardWindow.cs | 726 +++++ .../Editor/Setup/SetupWizardWindow.cs.meta | 11 + MCPForUnity/Editor/Tools.meta | 8 + MCPForUnity/Editor/Tools/CommandRegistry.cs | 138 + .../Editor/Tools/CommandRegistry.cs.meta | 11 + MCPForUnity/Editor/Tools/ManageAsset.cs | 1331 +++++++++ MCPForUnity/Editor/Tools/ManageAsset.cs.meta | 11 + MCPForUnity/Editor/Tools/ManageEditor.cs | 645 ++++ MCPForUnity/Editor/Tools/ManageEditor.cs.meta | 11 + MCPForUnity/Editor/Tools/ManageGameObject.cs | 2548 ++++++++++++++++ .../Editor/Tools/ManageGameObject.cs.meta | 11 + MCPForUnity/Editor/Tools/ManageScene.cs | 475 +++ MCPForUnity/Editor/Tools/ManageScene.cs.meta | 11 + MCPForUnity/Editor/Tools/ManageScript.cs | 2661 +++++++++++++++++ MCPForUnity/Editor/Tools/ManageScript.cs.meta | 11 + MCPForUnity/Editor/Tools/ManageShader.cs | 343 +++ MCPForUnity/Editor/Tools/ManageShader.cs.meta | 11 + .../Editor/Tools/McpForUnityToolAttribute.cs | 37 + .../Tools/McpForUnityToolAttribute.cs.meta | 11 + MCPForUnity/Editor/Tools/MenuItems.meta | 8 + .../Editor/Tools/MenuItems/ManageMenuItem.cs | 42 + .../Tools/MenuItems/ManageMenuItem.cs.meta | 11 + .../Tools/MenuItems/MenuItemExecutor.cs | 54 + .../Tools/MenuItems/MenuItemExecutor.cs.meta | 11 + .../Editor/Tools/MenuItems/MenuItemsReader.cs | 95 + .../Tools/MenuItems/MenuItemsReader.cs.meta | 11 + MCPForUnity/Editor/Tools/Prefabs.meta | 8 + .../Editor/Tools/Prefabs/ManagePrefabs.cs | 275 ++ .../Tools/Prefabs/ManagePrefabs.cs.meta | 11 + MCPForUnity/Editor/Tools/ReadConsole.cs | 575 ++++ MCPForUnity/Editor/Tools/ReadConsole.cs.meta | 11 + MCPForUnity/Editor/Windows.meta | 8 + .../Editor/Windows/MCPForUnityEditorWindow.cs | 1670 +++++++++++ .../Windows/MCPForUnityEditorWindow.cs.meta | 11 + .../Windows/ManualConfigEditorWindow.cs | 296 ++ .../Windows/ManualConfigEditorWindow.cs.meta | 11 + .../Editor/Windows/VSCodeManualSetupWindow.cs | 291 ++ .../Windows/VSCodeManualSetupWindow.cs.meta | 11 + MCPForUnity/README.md | 88 + MCPForUnity/README.md.meta | 7 + MCPForUnity/Runtime.meta | 8 + .../Runtime/MCPForUnity.Runtime.asmdef | 16 + .../Runtime/MCPForUnity.Runtime.asmdef.meta | 7 + MCPForUnity/Runtime/Serialization.meta | 8 + .../Serialization/UnityTypeConverters.cs | 266 ++ .../Serialization/UnityTypeConverters.cs.meta | 11 + MCPForUnity/UnityMcpServer~/src/Dockerfile | 27 + MCPForUnity/UnityMcpServer~/src/__init__.py | 3 + MCPForUnity/UnityMcpServer~/src/config.py | 48 + .../UnityMcpServer~/src/port_discovery.py | 160 + .../UnityMcpServer~/src/pyproject.toml | 15 + .../UnityMcpServer~/src/pyrightconfig.json | 11 + .../UnityMcpServer~/src/registry/__init__.py | 14 + .../src/registry/tool_registry.py | 51 + .../UnityMcpServer~/src/reload_sentinel.py | 9 + MCPForUnity/UnityMcpServer~/src/server.py | 194 ++ .../UnityMcpServer~/src/server_version.txt | 1 + MCPForUnity/UnityMcpServer~/src/telemetry.py | 460 +++ .../src/telemetry_decorator.py | 107 + .../UnityMcpServer~/src/test_telemetry.py | 161 + .../UnityMcpServer~/src/tools/__init__.py | 60 + .../UnityMcpServer~/src/tools/manage_asset.py | 83 + .../src/tools/manage_editor.py | 57 + .../src/tools/manage_gameobject.py | 145 + .../src/tools/manage_menu_item.py | 41 + .../src/tools/manage_prefabs.py | 58 + .../UnityMcpServer~/src/tools/manage_scene.py | 56 + .../src/tools/manage_script.py | 552 ++++ .../src/tools/manage_shader.py | 60 + .../UnityMcpServer~/src/tools/read_console.py | 87 + .../src/tools/resource_tools.py | 392 +++ .../src/tools/script_apply_edits.py | 966 ++++++ .../UnityMcpServer~/src/unity_connection.py | 452 +++ MCPForUnity/UnityMcpServer~/src/uv.lock | 400 +++ MCPForUnity/package.json | 26 + MCPForUnity/package.json.meta | 7 + README-zh.md | 2 +- README.md | 2 +- .../EditMode/Tools/CommandRegistryTests.cs | 7 + .../UnityMCPTests/Packages/manifest.json | 2 +- deploy-dev.bat | 4 +- docs/CUSTOM_TOOLS.md | 2 +- docs/README-DEV-zh.md | 2 +- docs/README-DEV.md | 2 +- docs/TELEMETRY.md | 2 +- mcp_source.py | 8 +- prune_tool_results.py | 2 +- restore-dev.bat | 2 +- tests/test_edit_normalization_and_noop.py | 2 +- tests/test_edit_strict_and_warnings.py | 2 +- tests/test_find_in_file_minimal.py | 2 +- tests/test_get_sha.py | 2 +- tests/test_improved_anchor_matching.py | 2 +- tests/test_logging_stdout.py | 2 +- tests/test_manage_script_uri.py | 2 +- tests/test_read_console_truncate.py | 2 +- tests/test_read_resource_minimal.py | 2 +- tests/test_resources_api.py | 2 +- tests/test_script_tools.py | 2 +- tests/test_telemetry_endpoint_validation.py | 8 +- tests/test_telemetry_queue_worker.py | 2 +- tests/test_telemetry_subaction.py | 2 +- tests/test_transport_framing.py | 2 +- tests/test_validate_script_summary.py | 2 +- 197 files changed, 26599 insertions(+), 55 deletions(-) create mode 100644 MCPForUnity/Editor.meta create mode 100644 MCPForUnity/Editor/AssemblyInfo.cs create mode 100644 MCPForUnity/Editor/AssemblyInfo.cs.meta create mode 100644 MCPForUnity/Editor/Data.meta create mode 100644 MCPForUnity/Editor/Data/DefaultServerConfig.cs create mode 100644 MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta create mode 100644 MCPForUnity/Editor/Data/McpClients.cs create mode 100644 MCPForUnity/Editor/Data/McpClients.cs.meta create mode 100644 MCPForUnity/Editor/Dependencies.meta create mode 100644 MCPForUnity/Editor/Dependencies/DependencyManager.cs create mode 100644 MCPForUnity/Editor/Dependencies/DependencyManager.cs.meta create mode 100644 MCPForUnity/Editor/Dependencies/Models.meta create mode 100644 MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs create mode 100644 MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs.meta create mode 100644 MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs create mode 100644 MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs.meta create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors.meta create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs create mode 100644 MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta create mode 100644 MCPForUnity/Editor/External.meta create mode 100644 MCPForUnity/Editor/External/Tommy.cs create mode 100644 MCPForUnity/Editor/External/Tommy.cs.meta create mode 100644 MCPForUnity/Editor/Helpers.meta create mode 100644 MCPForUnity/Editor/Helpers/AssetPathUtility.cs create mode 100644 MCPForUnity/Editor/Helpers/AssetPathUtility.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/CodexConfigHelper.cs create mode 100644 MCPForUnity/Editor/Helpers/CodexConfigHelper.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs create mode 100644 MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/ExecPath.cs create mode 100644 MCPForUnity/Editor/Helpers/ExecPath.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/GameObjectSerializer.cs create mode 100644 MCPForUnity/Editor/Helpers/GameObjectSerializer.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs create mode 100644 MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs create mode 100644 MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/McpLog.cs create mode 100644 MCPForUnity/Editor/Helpers/McpLog.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/McpPathResolver.cs create mode 100644 MCPForUnity/Editor/Helpers/McpPathResolver.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/PackageDetector.cs create mode 100644 MCPForUnity/Editor/Helpers/PackageDetector.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/PackageInstaller.cs create mode 100644 MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/PortManager.cs create mode 100644 MCPForUnity/Editor/Helpers/PortManager.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/Response.cs create mode 100644 MCPForUnity/Editor/Helpers/Response.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/ServerInstaller.cs create mode 100644 MCPForUnity/Editor/Helpers/ServerInstaller.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/ServerPathResolver.cs create mode 100644 MCPForUnity/Editor/Helpers/ServerPathResolver.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/TelemetryHelper.cs create mode 100644 MCPForUnity/Editor/Helpers/TelemetryHelper.cs.meta create mode 100644 MCPForUnity/Editor/Helpers/Vector3Helper.cs create mode 100644 MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta create mode 100644 MCPForUnity/Editor/MCPForUnity.Editor.asmdef create mode 100644 MCPForUnity/Editor/MCPForUnity.Editor.asmdef.meta create mode 100644 MCPForUnity/Editor/MCPForUnityBridge.cs create mode 100644 MCPForUnity/Editor/MCPForUnityBridge.cs.meta create mode 100644 MCPForUnity/Editor/Models.meta create mode 100644 MCPForUnity/Editor/Models/Command.cs create mode 100644 MCPForUnity/Editor/Models/Command.cs.meta create mode 100644 MCPForUnity/Editor/Models/MCPConfigServer.cs create mode 100644 MCPForUnity/Editor/Models/MCPConfigServer.cs.meta create mode 100644 MCPForUnity/Editor/Models/MCPConfigServers.cs create mode 100644 MCPForUnity/Editor/Models/MCPConfigServers.cs.meta create mode 100644 MCPForUnity/Editor/Models/McpClient.cs create mode 100644 MCPForUnity/Editor/Models/McpClient.cs.meta create mode 100644 MCPForUnity/Editor/Models/McpConfig.cs create mode 100644 MCPForUnity/Editor/Models/McpConfig.cs.meta create mode 100644 MCPForUnity/Editor/Models/McpStatus.cs create mode 100644 MCPForUnity/Editor/Models/McpStatus.cs.meta create mode 100644 MCPForUnity/Editor/Models/McpTypes.cs create mode 100644 MCPForUnity/Editor/Models/McpTypes.cs.meta create mode 100644 MCPForUnity/Editor/Models/ServerConfig.cs create mode 100644 MCPForUnity/Editor/Models/ServerConfig.cs.meta create mode 100644 MCPForUnity/Editor/Setup.meta create mode 100644 MCPForUnity/Editor/Setup/SetupWizard.cs create mode 100644 MCPForUnity/Editor/Setup/SetupWizard.cs.meta create mode 100644 MCPForUnity/Editor/Setup/SetupWizardWindow.cs create mode 100644 MCPForUnity/Editor/Setup/SetupWizardWindow.cs.meta create mode 100644 MCPForUnity/Editor/Tools.meta create mode 100644 MCPForUnity/Editor/Tools/CommandRegistry.cs create mode 100644 MCPForUnity/Editor/Tools/CommandRegistry.cs.meta create mode 100644 MCPForUnity/Editor/Tools/ManageAsset.cs create mode 100644 MCPForUnity/Editor/Tools/ManageAsset.cs.meta create mode 100644 MCPForUnity/Editor/Tools/ManageEditor.cs create mode 100644 MCPForUnity/Editor/Tools/ManageEditor.cs.meta create mode 100644 MCPForUnity/Editor/Tools/ManageGameObject.cs create mode 100644 MCPForUnity/Editor/Tools/ManageGameObject.cs.meta create mode 100644 MCPForUnity/Editor/Tools/ManageScene.cs create mode 100644 MCPForUnity/Editor/Tools/ManageScene.cs.meta create mode 100644 MCPForUnity/Editor/Tools/ManageScript.cs create mode 100644 MCPForUnity/Editor/Tools/ManageScript.cs.meta create mode 100644 MCPForUnity/Editor/Tools/ManageShader.cs create mode 100644 MCPForUnity/Editor/Tools/ManageShader.cs.meta create mode 100644 MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs create mode 100644 MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs.meta create mode 100644 MCPForUnity/Editor/Tools/MenuItems.meta create mode 100644 MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs create mode 100644 MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs.meta create mode 100644 MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs create mode 100644 MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta create mode 100644 MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs create mode 100644 MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs.meta create mode 100644 MCPForUnity/Editor/Tools/Prefabs.meta create mode 100644 MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs create mode 100644 MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs.meta create mode 100644 MCPForUnity/Editor/Tools/ReadConsole.cs create mode 100644 MCPForUnity/Editor/Tools/ReadConsole.cs.meta create mode 100644 MCPForUnity/Editor/Windows.meta create mode 100644 MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs create mode 100644 MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs.meta create mode 100644 MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs create mode 100644 MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs.meta create mode 100644 MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs create mode 100644 MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs.meta create mode 100644 MCPForUnity/README.md create mode 100644 MCPForUnity/README.md.meta create mode 100644 MCPForUnity/Runtime.meta create mode 100644 MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef create mode 100644 MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef.meta create mode 100644 MCPForUnity/Runtime/Serialization.meta create mode 100644 MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs create mode 100644 MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs.meta create mode 100644 MCPForUnity/UnityMcpServer~/src/Dockerfile create mode 100644 MCPForUnity/UnityMcpServer~/src/__init__.py create mode 100644 MCPForUnity/UnityMcpServer~/src/config.py create mode 100644 MCPForUnity/UnityMcpServer~/src/port_discovery.py create mode 100644 MCPForUnity/UnityMcpServer~/src/pyproject.toml create mode 100644 MCPForUnity/UnityMcpServer~/src/pyrightconfig.json create mode 100644 MCPForUnity/UnityMcpServer~/src/registry/__init__.py create mode 100644 MCPForUnity/UnityMcpServer~/src/registry/tool_registry.py create mode 100644 MCPForUnity/UnityMcpServer~/src/reload_sentinel.py create mode 100644 MCPForUnity/UnityMcpServer~/src/server.py create mode 100644 MCPForUnity/UnityMcpServer~/src/server_version.txt create mode 100644 MCPForUnity/UnityMcpServer~/src/telemetry.py create mode 100644 MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py create mode 100644 MCPForUnity/UnityMcpServer~/src/test_telemetry.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/__init__.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_script.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/read_console.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py create mode 100644 MCPForUnity/UnityMcpServer~/src/unity_connection.py create mode 100644 MCPForUnity/UnityMcpServer~/src/uv.lock create mode 100644 MCPForUnity/package.json create mode 100644 MCPForUnity/package.json.meta diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 8f562c30..3ce232b7 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -31,7 +31,7 @@ jobs: run: | set -euo pipefail BUMP="${{ inputs.version_bump }}" - CURRENT_VERSION=$(jq -r '.version' "UnityMcpBridge/package.json") + CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json") echo "Current version: $CURRENT_VERSION" IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION" @@ -63,15 +63,15 @@ jobs: run: | set -euo pipefail - echo "Updating UnityMcpBridge/package.json to $NEW_VERSION" - jq ".version = \"${NEW_VERSION}\"" UnityMcpBridge/package.json > UnityMcpBridge/package.json.tmp - mv UnityMcpBridge/package.json.tmp UnityMcpBridge/package.json + echo "Updating MCPForUnity/package.json to $NEW_VERSION" + jq ".version = \"${NEW_VERSION}\"" MCPForUnity/package.json > MCPForUnity/package.json.tmp + mv MCPForUnity/package.json.tmp MCPForUnity/package.json - echo "Updating UnityMcpBridge/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION" - sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "UnityMcpBridge/UnityMcpServer~/src/pyproject.toml" + echo "Updating MCPForUnity/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION" + sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "MCPForUnity/UnityMcpServer~/src/pyproject.toml" - echo "Updating UnityMcpBridge/UnityMcpServer~/src/server_version.txt to $NEW_VERSION" - echo "$NEW_VERSION" > "UnityMcpBridge/UnityMcpServer~/src/server_version.txt" + echo "Updating MCPForUnity/UnityMcpServer~/src/server_version.txt to $NEW_VERSION" + echo "$NEW_VERSION" > "MCPForUnity/UnityMcpServer~/src/server_version.txt" - name: Commit and push changes env: @@ -81,7 +81,7 @@ jobs: set -euo pipefail git config user.name "GitHub Actions" git config user.email "actions@github.com" - git add UnityMcpBridge/package.json "UnityMcpBridge/UnityMcpServer~/src/pyproject.toml" "UnityMcpBridge/UnityMcpServer~/src/server_version.txt" + git add MCPForUnity/package.json "MCPForUnity/UnityMcpServer~/src/pyproject.toml" "MCPForUnity/UnityMcpServer~/src/server_version.txt" if git diff --cached --quiet; then echo "No version changes to commit." else diff --git a/.github/workflows/claude-nl-suite.yml b/.github/workflows/claude-nl-suite.yml index 09176a3a..e5b88763 100644 --- a/.github/workflows/claude-nl-suite.yml +++ b/.github/workflows/claude-nl-suite.yml @@ -55,14 +55,14 @@ jobs: uv venv echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV" echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" - if [ -f UnityMcpBridge/UnityMcpServer~/src/pyproject.toml ]; then - uv pip install -e UnityMcpBridge/UnityMcpServer~/src - elif [ -f UnityMcpBridge/UnityMcpServer~/src/requirements.txt ]; then - uv pip install -r UnityMcpBridge/UnityMcpServer~/src/requirements.txt - elif [ -f UnityMcpBridge/UnityMcpServer~/pyproject.toml ]; then - uv pip install -e UnityMcpBridge/UnityMcpServer~/ - elif [ -f UnityMcpBridge/UnityMcpServer~/requirements.txt ]; then - uv pip install -r UnityMcpBridge/UnityMcpServer~/requirements.txt + if [ -f MCPForUnity/UnityMcpServer~/src/pyproject.toml ]; then + uv pip install -e MCPForUnity/UnityMcpServer~/src + elif [ -f MCPForUnity/UnityMcpServer~/src/requirements.txt ]; then + uv pip install -r MCPForUnity/UnityMcpServer~/src/requirements.txt + elif [ -f MCPForUnity/UnityMcpServer~/pyproject.toml ]; then + uv pip install -e MCPForUnity/UnityMcpServer~/ + elif [ -f MCPForUnity/UnityMcpServer~/requirements.txt ]; then + uv pip install -r MCPForUnity/UnityMcpServer~/requirements.txt else echo "No MCP Python deps found (skipping)" fi @@ -285,7 +285,7 @@ jobs: "mcpServers": { "unity": { "command": "uv", - "args": ["run","--active","--directory","UnityMcpBridge/UnityMcpServer~/src","python","server.py"], + "args": ["run","--active","--directory","MCPForUnity/UnityMcpServer~/src","python","server.py"], "transport": { "type": "stdio" }, "env": { "PYTHONUNBUFFERED": "1", diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index 0b230966..4b795ad1 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -5,7 +5,7 @@ on: branches: [main] paths: - TestProjects/UnityMCPTests/** - - UnityMcpBridge/Editor/** + - MCPForUnity/Editor/** - .github/workflows/unity-tests.yml jobs: diff --git a/.gitignore b/.gitignore index be9fc7e3..d82423e0 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ UnityMcpServer.meta # Unity Editor *.unitypackage *.asset -UnityMcpBridge.meta LICENSE.meta CONTRIBUTING.md.meta diff --git a/MCPForUnity/Editor.meta b/MCPForUnity/Editor.meta new file mode 100644 index 00000000..26495d40 --- /dev/null +++ b/MCPForUnity/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 31e7fac5858840340a75cc6df0ad3d9e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/AssemblyInfo.cs b/MCPForUnity/Editor/AssemblyInfo.cs new file mode 100644 index 00000000..bae75b67 --- /dev/null +++ b/MCPForUnity/Editor/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")] diff --git a/MCPForUnity/Editor/AssemblyInfo.cs.meta b/MCPForUnity/Editor/AssemblyInfo.cs.meta new file mode 100644 index 00000000..72bf5f72 --- /dev/null +++ b/MCPForUnity/Editor/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be61633e00d934610ac1ff8192ffbe3d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Data.meta b/MCPForUnity/Editor/Data.meta new file mode 100644 index 00000000..bb714ec4 --- /dev/null +++ b/MCPForUnity/Editor/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e59036660cc33d24596fbbf6d4657a83 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Data/DefaultServerConfig.cs b/MCPForUnity/Editor/Data/DefaultServerConfig.cs new file mode 100644 index 00000000..59cced75 --- /dev/null +++ b/MCPForUnity/Editor/Data/DefaultServerConfig.cs @@ -0,0 +1,17 @@ +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Data +{ + public class DefaultServerConfig : ServerConfig + { + public new string unityHost = "localhost"; + public new int unityPort = 6400; + public new int mcpPort = 6500; + public new float connectionTimeout = 15.0f; + public new int bufferSize = 32768; + public new string logLevel = "INFO"; + public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"; + public new int maxRetries = 3; + public new float retryDelay = 1.0f; + } +} diff --git a/MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta b/MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta new file mode 100644 index 00000000..82e437f2 --- /dev/null +++ b/MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de8f5721c34f7194392e9d8c7d0226c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Data/McpClients.cs b/MCPForUnity/Editor/Data/McpClients.cs new file mode 100644 index 00000000..9e718847 --- /dev/null +++ b/MCPForUnity/Editor/Data/McpClients.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Data +{ + public class McpClients + { + public List clients = new() + { + // 1) Cursor + new() + { + name = "Cursor", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cursor", + "mcp.json" + ), + macConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cursor", + "mcp.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cursor", + "mcp.json" + ), + mcpType = McpTypes.Cursor, + configStatus = "Not Configured", + }, + // 2) Claude Code + new() + { + name = "Claude Code", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".claude.json" + ), + macConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".claude.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".claude.json" + ), + mcpType = McpTypes.ClaudeCode, + configStatus = "Not Configured", + }, + // 3) Windsurf + new() + { + name = "Windsurf", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".codeium", + "windsurf", + "mcp_config.json" + ), + macConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".codeium", + "windsurf", + "mcp_config.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".codeium", + "windsurf", + "mcp_config.json" + ), + mcpType = McpTypes.Windsurf, + configStatus = "Not Configured", + }, + // 4) Claude Desktop + new() + { + name = "Claude Desktop", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Claude", + "claude_desktop_config.json" + ), + + macConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Claude", + "claude_desktop_config.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config", + "Claude", + "claude_desktop_config.json" + ), + + mcpType = McpTypes.ClaudeDesktop, + configStatus = "Not Configured", + }, + // 5) VSCode GitHub Copilot + new() + { + name = "VSCode GitHub Copilot", + // Windows path is canonical under %AppData%\Code\User + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Code", + "User", + "mcp.json" + ), + // macOS: ~/Library/Application Support/Code/User/mcp.json + macConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Code", + "User", + "mcp.json" + ), + // Linux: ~/.config/Code/User/mcp.json + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config", + "Code", + "User", + "mcp.json" + ), + mcpType = McpTypes.VSCode, + configStatus = "Not Configured", + }, + // 3) Kiro + new() + { + name = "Kiro", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".kiro", + "settings", + "mcp.json" + ), + macConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".kiro", + "settings", + "mcp.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".kiro", + "settings", + "mcp.json" + ), + mcpType = McpTypes.Kiro, + configStatus = "Not Configured", + }, + // 4) Codex CLI + new() + { + name = "Codex CLI", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".codex", + "config.toml" + ), + macConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".codex", + "config.toml" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".codex", + "config.toml" + ), + mcpType = McpTypes.Codex, + configStatus = "Not Configured", + }, + }; + + // Initialize status enums after construction + public McpClients() + { + foreach (var client in clients) + { + if (client.configStatus == "Not Configured") + { + client.status = McpStatus.NotConfigured; + } + } + } + } +} diff --git a/MCPForUnity/Editor/Data/McpClients.cs.meta b/MCPForUnity/Editor/Data/McpClients.cs.meta new file mode 100644 index 00000000..e5a10813 --- /dev/null +++ b/MCPForUnity/Editor/Data/McpClients.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 711b86bbc1f661e4fb2c822e14970e16 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Dependencies.meta b/MCPForUnity/Editor/Dependencies.meta new file mode 100644 index 00000000..77685d17 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 221a4d6e595be6897a5b17b77aedd4d0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Dependencies/DependencyManager.cs b/MCPForUnity/Editor/Dependencies/DependencyManager.cs new file mode 100644 index 00000000..ce6efef2 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/DependencyManager.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Dependencies.PlatformDetectors; +using MCPForUnity.Editor.Helpers; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Dependencies +{ + /// + /// Main orchestrator for dependency validation and management + /// + public static class DependencyManager + { + private static readonly List _detectors = new List + { + new WindowsPlatformDetector(), + new MacOSPlatformDetector(), + new LinuxPlatformDetector() + }; + + private static IPlatformDetector _currentDetector; + + /// + /// Get the platform detector for the current operating system + /// + public static IPlatformDetector GetCurrentPlatformDetector() + { + if (_currentDetector == null) + { + _currentDetector = _detectors.FirstOrDefault(d => d.CanDetect); + if (_currentDetector == null) + { + throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}"); + } + } + return _currentDetector; + } + + /// + /// Perform a comprehensive dependency check + /// + public static DependencyCheckResult CheckAllDependencies() + { + var result = new DependencyCheckResult(); + + try + { + var detector = GetCurrentPlatformDetector(); + McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false); + + // Check Python + var pythonStatus = detector.DetectPython(); + result.Dependencies.Add(pythonStatus); + + // Check UV + var uvStatus = detector.DetectUV(); + result.Dependencies.Add(uvStatus); + + // Check MCP Server + var serverStatus = detector.DetectMCPServer(); + result.Dependencies.Add(serverStatus); + + // Generate summary and recommendations + result.GenerateSummary(); + GenerateRecommendations(result, detector); + + McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false); + } + catch (Exception ex) + { + McpLog.Error($"Error during dependency check: {ex.Message}"); + result.Summary = $"Dependency check failed: {ex.Message}"; + result.IsSystemReady = false; + } + + return result; + } + + /// + /// Get installation recommendations for the current platform + /// + public static string GetInstallationRecommendations() + { + try + { + var detector = GetCurrentPlatformDetector(); + return detector.GetInstallationRecommendations(); + } + catch (Exception ex) + { + return $"Error getting installation recommendations: {ex.Message}"; + } + } + + /// + /// Get platform-specific installation URLs + /// + public static (string pythonUrl, string uvUrl) GetInstallationUrls() + { + try + { + var detector = GetCurrentPlatformDetector(); + return (detector.GetPythonInstallUrl(), detector.GetUVInstallUrl()); + } + catch + { + return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/"); + } + } + + private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector) + { + var missing = result.GetMissingDependencies(); + + if (missing.Count == 0) + { + result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity."); + return; + } + + foreach (var dep in missing) + { + if (dep.Name == "Python") + { + result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}"); + } + else if (dep.Name == "UV Package Manager") + { + result.RecommendedActions.Add($"Install UV package manager from: {detector.GetUVInstallUrl()}"); + } + else if (dep.Name == "MCP Server") + { + result.RecommendedActions.Add("MCP Server will be installed automatically when needed."); + } + } + + if (result.GetMissingRequired().Count > 0) + { + result.RecommendedActions.Add("Use the Setup Wizard (Window > MCP for Unity > Setup Wizard) for guided installation."); + } + } + } +} diff --git a/MCPForUnity/Editor/Dependencies/DependencyManager.cs.meta b/MCPForUnity/Editor/Dependencies/DependencyManager.cs.meta new file mode 100644 index 00000000..ae03260a --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/DependencyManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6789012345678901234abcdef012345 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Dependencies/Models.meta b/MCPForUnity/Editor/Dependencies/Models.meta new file mode 100644 index 00000000..2174dd52 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/Models.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b2c3d4e5f6789012345678901234abcd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs b/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs new file mode 100644 index 00000000..5dd2edaf --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MCPForUnity.Editor.Dependencies.Models +{ + /// + /// Result of a comprehensive dependency check + /// + [Serializable] + public class DependencyCheckResult + { + /// + /// List of all dependency statuses checked + /// + public List Dependencies { get; set; } + + /// + /// Overall system readiness for MCP operations + /// + public bool IsSystemReady { get; set; } + + /// + /// Whether all required dependencies are available + /// + public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false; + + /// + /// Whether any optional dependencies are missing + /// + public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false; + + /// + /// Summary message about the dependency state + /// + public string Summary { get; set; } + + /// + /// Recommended next steps for the user + /// + public List RecommendedActions { get; set; } + + /// + /// Timestamp when this check was performed + /// + public DateTime CheckedAt { get; set; } + + public DependencyCheckResult() + { + Dependencies = new List(); + RecommendedActions = new List(); + CheckedAt = DateTime.UtcNow; + } + + /// + /// Get dependencies by availability status + /// + public List GetMissingDependencies() + { + return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List(); + } + + /// + /// Get missing required dependencies + /// + public List GetMissingRequired() + { + return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List(); + } + + /// + /// Generate a user-friendly summary of the dependency state + /// + public void GenerateSummary() + { + var missing = GetMissingDependencies(); + var missingRequired = GetMissingRequired(); + + if (missing.Count == 0) + { + Summary = "All dependencies are available and ready."; + IsSystemReady = true; + } + else if (missingRequired.Count == 0) + { + Summary = $"System is ready. {missing.Count} optional dependencies are missing."; + IsSystemReady = true; + } + else + { + Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing."; + IsSystemReady = false; + } + } + } +} diff --git a/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs.meta b/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs.meta new file mode 100644 index 00000000..a88c3bb2 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 789012345678901234abcdef01234567 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs b/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs new file mode 100644 index 00000000..e755ecad --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs @@ -0,0 +1,65 @@ +using System; + +namespace MCPForUnity.Editor.Dependencies.Models +{ + /// + /// Represents the status of a dependency check + /// + [Serializable] + public class DependencyStatus + { + /// + /// Name of the dependency being checked + /// + public string Name { get; set; } + + /// + /// Whether the dependency is available and functional + /// + public bool IsAvailable { get; set; } + + /// + /// Version information if available + /// + public string Version { get; set; } + + /// + /// Path to the dependency executable/installation + /// + public string Path { get; set; } + + /// + /// Additional details about the dependency status + /// + public string Details { get; set; } + + /// + /// Error message if dependency check failed + /// + public string ErrorMessage { get; set; } + + /// + /// Whether this dependency is required for basic functionality + /// + public bool IsRequired { get; set; } + + /// + /// Suggested installation method or URL + /// + public string InstallationHint { get; set; } + + public DependencyStatus(string name, bool isRequired = true) + { + Name = name; + IsRequired = isRequired; + IsAvailable = false; + } + + public override string ToString() + { + var status = IsAvailable ? "✓" : "✗"; + var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : ""; + return $"{status} {Name}{version}"; + } + } +} diff --git a/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs.meta b/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs.meta new file mode 100644 index 00000000..d6eb1d59 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6789012345678901234abcdef0123456 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta b/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta new file mode 100644 index 00000000..22a6b1db --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c3d4e5f6789012345678901234abcdef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs new file mode 100644 index 00000000..7fba58f9 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs @@ -0,0 +1,50 @@ +using MCPForUnity.Editor.Dependencies.Models; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Interface for platform-specific dependency detection + /// + public interface IPlatformDetector + { + /// + /// Platform name this detector handles + /// + string PlatformName { get; } + + /// + /// Whether this detector can run on the current platform + /// + bool CanDetect { get; } + + /// + /// Detect Python installation on this platform + /// + DependencyStatus DetectPython(); + + /// + /// Detect UV package manager on this platform + /// + DependencyStatus DetectUV(); + + /// + /// Detect MCP server installation on this platform + /// + DependencyStatus DetectMCPServer(); + + /// + /// Get platform-specific installation recommendations + /// + string GetInstallationRecommendations(); + + /// + /// Get platform-specific Python installation URL + /// + string GetPythonInstallUrl(); + + /// + /// Get platform-specific UV installation URL + /// + string GetUVInstallUrl(); + } +} diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta b/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta new file mode 100644 index 00000000..d2cd9f07 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9012345678901234abcdef0123456789 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs new file mode 100644 index 00000000..4ace9756 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs @@ -0,0 +1,212 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Linux-specific dependency detection + /// + public class LinuxPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "Linux"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths on Linux + var candidates = new[] + { + "python3", + "python", + "/usr/bin/python3", + "/usr/local/bin/python3", + "/opt/python/bin/python3", + "/snap/bin/python3" + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'which' command + if (TryFindInPath("python3", out string pathResult) || + TryFindInPath("python", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths including system, snap, and user-local locations."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://www.python.org/downloads/source/"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#linux"; + } + + public override string GetInstallationRecommendations() + { + return @"Linux Installation Recommendations: + +1. Python: Install via package manager or pyenv + - Ubuntu/Debian: sudo apt install python3 python3-pip + - Fedora/RHEL: sudo dnf install python3 python3-pip + - Arch: sudo pacman -S python python-pip + - Or use pyenv: https://github.com/pyenv/pyenv + +2. UV Package Manager: Install via curl + - Run: curl -LsSf https://astral.sh/uv/install.sh | sh + - Or download from: https://github.com/astral-sh/uv/releases + +3. MCP Server: Will be installed automatically by Unity MCP Bridge + +Note: Make sure ~/.local/bin is in your PATH for user-local installations."; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Set PATH to include common locations + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/usr/local/bin", + "/usr/bin", + "/bin", + "/snap/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Enhance PATH for Unity's GUI environment + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/usr/local/bin", + "/usr/bin", + "/bin", + "/snap/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + fullPath = output; + return true; + } + } + catch + { + // Ignore errors + } + + return false; + } + } +} diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta new file mode 100644 index 00000000..4f8267fd --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2345678901234abcdef0123456789abc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs new file mode 100644 index 00000000..c89e7cb9 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs @@ -0,0 +1,212 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// macOS-specific dependency detection + /// + public class MacOSPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "macOS"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths on macOS + var candidates = new[] + { + "python3", + "python", + "/usr/bin/python3", + "/usr/local/bin/python3", + "/opt/homebrew/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.10/bin/python3" + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'which' command + if (TryFindInPath("python3", out string pathResult) || + TryFindInPath("python", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths including Homebrew, Framework, and system locations."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://www.python.org/downloads/macos/"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#macos"; + } + + public override string GetInstallationRecommendations() + { + return @"macOS Installation Recommendations: + +1. Python: Install via Homebrew (recommended) or python.org + - Homebrew: brew install python3 + - Direct download: https://python.org/downloads/macos/ + +2. UV Package Manager: Install via curl or Homebrew + - Curl: curl -LsSf https://astral.sh/uv/install.sh | sh + - Homebrew: brew install uv + +3. MCP Server: Will be installed automatically by Unity MCP Bridge + +Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH."; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Set PATH to include common locations + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Enhance PATH for Unity's GUI environment + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + fullPath = output; + return true; + } + } + catch + { + // Ignore errors + } + + return false; + } + } +} diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta new file mode 100644 index 00000000..b43864a2 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12345678901234abcdef0123456789ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs new file mode 100644 index 00000000..98044f17 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs @@ -0,0 +1,161 @@ +using System; +using System.Diagnostics; +using System.IO; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Base class for platform-specific dependency detection + /// + public abstract class PlatformDetectorBase : IPlatformDetector + { + public abstract string PlatformName { get; } + public abstract bool CanDetect { get; } + + public abstract DependencyStatus DetectPython(); + public abstract string GetPythonInstallUrl(); + public abstract string GetUVInstallUrl(); + public abstract string GetInstallationRecommendations(); + + public virtual DependencyStatus DetectUV() + { + var status = new DependencyStatus("UV Package Manager", isRequired: true) + { + InstallationHint = GetUVInstallUrl() + }; + + try + { + // Use existing UV detection from ServerInstaller + string uvPath = ServerInstaller.FindUvPath(); + if (!string.IsNullOrEmpty(uvPath)) + { + if (TryValidateUV(uvPath, out string version)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = uvPath; + status.Details = $"Found UV {version} at {uvPath}"; + return status; + } + } + + status.ErrorMessage = "UV package manager not found. Please install UV."; + status.Details = "UV is required for managing Python dependencies."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting UV: {ex.Message}"; + } + + return status; + } + + public virtual DependencyStatus DetectMCPServer() + { + var status = new DependencyStatus("MCP Server", isRequired: false); + + try + { + // Check if server is installed + string serverPath = ServerInstaller.GetServerPath(); + string serverPy = Path.Combine(serverPath, "server.py"); + + if (File.Exists(serverPy)) + { + status.IsAvailable = true; + status.Path = serverPath; + + // Try to get version + string versionFile = Path.Combine(serverPath, "server_version.txt"); + if (File.Exists(versionFile)) + { + status.Version = File.ReadAllText(versionFile).Trim(); + } + + status.Details = $"MCP Server found at {serverPath}"; + } + else + { + // Check for embedded server + if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath)) + { + status.IsAvailable = true; + status.Path = embeddedPath; + status.Details = "MCP Server available (embedded in package)"; + } + else + { + status.ErrorMessage = "MCP Server not found"; + status.Details = "Server will be installed automatically when needed"; + } + } + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}"; + } + + return status; + } + + protected bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + protected bool TryParseVersion(string version, out int major, out int minor) + { + major = 0; + minor = 0; + + try + { + var parts = version.Split('.'); + if (parts.Length >= 2) + { + return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor); + } + } + catch + { + // Ignore parsing errors + } + + return false; + } + } +} diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta b/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta new file mode 100644 index 00000000..4821e757 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44d715aedea2b8b41bf914433bbb2c49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs new file mode 100644 index 00000000..bd9c6f03 --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs @@ -0,0 +1,191 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Windows-specific dependency detection + /// + public class WindowsPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "Windows"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths + var candidates = new[] + { + "python.exe", + "python3.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python313", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python312", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python311", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "Python313", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "Python312", "python.exe") + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'where' command + if (TryFindInPath("python.exe", out string pathResult) || + TryFindInPath("python3.exe", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths and PATH environment variable."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#windows"; + } + + public override string GetInstallationRecommendations() + { + return @"Windows Installation Recommendations: + +1. Python: Install from Microsoft Store or python.org + - Microsoft Store: Search for 'Python 3.12' or 'Python 3.13' + - Direct download: https://python.org/downloads/windows/ + +2. UV Package Manager: Install via PowerShell + - Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex"" + - Or download from: https://github.com/astral-sh/uv/releases + +3. MCP Server: Will be installed automatically by Unity MCP Bridge"; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "where", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output)) + { + // Take the first result + var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + if (lines.Length > 0) + { + fullPath = lines[0].Trim(); + return File.Exists(fullPath); + } + } + } + catch + { + // Ignore errors + } + + return false; + } + } +} diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta new file mode 100644 index 00000000..e7e53d7d --- /dev/null +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 012345678901234abcdef0123456789a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/External.meta b/MCPForUnity/Editor/External.meta new file mode 100644 index 00000000..ce757b15 --- /dev/null +++ b/MCPForUnity/Editor/External.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c11944bcfb9ec4576bab52874b7df584 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/External/Tommy.cs b/MCPForUnity/Editor/External/Tommy.cs new file mode 100644 index 00000000..22e83b81 --- /dev/null +++ b/MCPForUnity/Editor/External/Tommy.cs @@ -0,0 +1,2138 @@ +#region LICENSE + +/* + * MIT License + * + * Copyright (c) 2020 Denis Zhidkikh + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace MCPForUnity.External.Tommy +{ + #region TOML Nodes + + public abstract class TomlNode : IEnumerable + { + public virtual bool HasValue { get; } = false; + public virtual bool IsArray { get; } = false; + public virtual bool IsTable { get; } = false; + public virtual bool IsString { get; } = false; + public virtual bool IsInteger { get; } = false; + public virtual bool IsFloat { get; } = false; + public bool IsDateTime => IsDateTimeLocal || IsDateTimeOffset; + public virtual bool IsDateTimeLocal { get; } = false; + public virtual bool IsDateTimeOffset { get; } = false; + public virtual bool IsBoolean { get; } = false; + public virtual string Comment { get; set; } + public virtual int CollapseLevel { get; set; } + + public virtual TomlTable AsTable => this as TomlTable; + public virtual TomlString AsString => this as TomlString; + public virtual TomlInteger AsInteger => this as TomlInteger; + public virtual TomlFloat AsFloat => this as TomlFloat; + public virtual TomlBoolean AsBoolean => this as TomlBoolean; + public virtual TomlDateTimeLocal AsDateTimeLocal => this as TomlDateTimeLocal; + public virtual TomlDateTimeOffset AsDateTimeOffset => this as TomlDateTimeOffset; + public virtual TomlDateTime AsDateTime => this as TomlDateTime; + public virtual TomlArray AsArray => this as TomlArray; + + public virtual int ChildrenCount => 0; + + public virtual TomlNode this[string key] + { + get => null; + set { } + } + + public virtual TomlNode this[int index] + { + get => null; + set { } + } + + public virtual IEnumerable Children + { + get { yield break; } + } + + public virtual IEnumerable Keys + { + get { yield break; } + } + + public IEnumerator GetEnumerator() => Children.GetEnumerator(); + + public virtual bool TryGetNode(string key, out TomlNode node) + { + node = null; + return false; + } + + public virtual bool HasKey(string key) => false; + + public virtual bool HasItemAt(int index) => false; + + public virtual void Add(string key, TomlNode node) { } + + public virtual void Add(TomlNode node) { } + + public virtual void Delete(TomlNode node) { } + + public virtual void Delete(string key) { } + + public virtual void Delete(int index) { } + + public virtual void AddRange(IEnumerable nodes) + { + foreach (var tomlNode in nodes) Add(tomlNode); + } + + public virtual void WriteTo(TextWriter tw, string name = null) => tw.WriteLine(ToInlineToml()); + + public virtual string ToInlineToml() => ToString(); + + #region Native type to TOML cast + + public static implicit operator TomlNode(string value) => new TomlString { Value = value }; + + public static implicit operator TomlNode(bool value) => new TomlBoolean { Value = value }; + + public static implicit operator TomlNode(long value) => new TomlInteger { Value = value }; + + public static implicit operator TomlNode(float value) => new TomlFloat { Value = value }; + + public static implicit operator TomlNode(double value) => new TomlFloat { Value = value }; + + public static implicit operator TomlNode(DateTime value) => new TomlDateTimeLocal { Value = value }; + + public static implicit operator TomlNode(DateTimeOffset value) => new TomlDateTimeOffset { Value = value }; + + public static implicit operator TomlNode(TomlNode[] nodes) + { + var result = new TomlArray(); + result.AddRange(nodes); + return result; + } + + #endregion + + #region TOML to native type cast + + public static implicit operator string(TomlNode value) => value.ToString(); + + public static implicit operator int(TomlNode value) => (int)value.AsInteger.Value; + + public static implicit operator long(TomlNode value) => value.AsInteger.Value; + + public static implicit operator float(TomlNode value) => (float)value.AsFloat.Value; + + public static implicit operator double(TomlNode value) => value.AsFloat.Value; + + public static implicit operator bool(TomlNode value) => value.AsBoolean.Value; + + public static implicit operator DateTime(TomlNode value) => value.AsDateTimeLocal.Value; + + public static implicit operator DateTimeOffset(TomlNode value) => value.AsDateTimeOffset.Value; + + #endregion + } + + public class TomlString : TomlNode + { + public override bool HasValue { get; } = true; + public override bool IsString { get; } = true; + public bool IsMultiline { get; set; } + public bool MultilineTrimFirstLine { get; set; } + public bool PreferLiteral { get; set; } + + public string Value { get; set; } + + public override string ToString() => Value; + + public override string ToInlineToml() + { + // Automatically convert literal to non-literal if there are too many literal string symbols + if (Value.IndexOf(new string(TomlSyntax.LITERAL_STRING_SYMBOL, IsMultiline ? 3 : 1), StringComparison.Ordinal) != -1 && PreferLiteral) PreferLiteral = false; + var quotes = new string(PreferLiteral ? TomlSyntax.LITERAL_STRING_SYMBOL : TomlSyntax.BASIC_STRING_SYMBOL, + IsMultiline ? 3 : 1); + var result = PreferLiteral ? Value : Value.Escape(!IsMultiline); + if (IsMultiline) + result = result.Replace("\r\n", "\n").Replace("\n", Environment.NewLine); + if (IsMultiline && (MultilineTrimFirstLine || !MultilineTrimFirstLine && result.StartsWith(Environment.NewLine))) + result = $"{Environment.NewLine}{result}"; + return $"{quotes}{result}{quotes}"; + } + } + + public class TomlInteger : TomlNode + { + public enum Base + { + Binary = 2, + Octal = 8, + Decimal = 10, + Hexadecimal = 16 + } + + public override bool IsInteger { get; } = true; + public override bool HasValue { get; } = true; + public Base IntegerBase { get; set; } = Base.Decimal; + + public long Value { get; set; } + + public override string ToString() => Value.ToString(); + + public override string ToInlineToml() => + IntegerBase != Base.Decimal + ? $"0{TomlSyntax.BaseIdentifiers[(int)IntegerBase]}{Convert.ToString(Value, (int)IntegerBase)}" + : Value.ToString(CultureInfo.InvariantCulture); + } + + public class TomlFloat : TomlNode, IFormattable + { + public override bool IsFloat { get; } = true; + public override bool HasValue { get; } = true; + + public double Value { get; set; } + + public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); + + public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider); + + public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); + + public override string ToInlineToml() => + Value switch + { + var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE, + var v when double.IsPositiveInfinity(v) => TomlSyntax.INF_VALUE, + var v when double.IsNegativeInfinity(v) => TomlSyntax.NEG_INF_VALUE, + var v => v.ToString("G", CultureInfo.InvariantCulture).ToLowerInvariant() + }; + } + + public class TomlBoolean : TomlNode + { + public override bool IsBoolean { get; } = true; + public override bool HasValue { get; } = true; + + public bool Value { get; set; } + + public override string ToString() => Value.ToString(); + + public override string ToInlineToml() => Value ? TomlSyntax.TRUE_VALUE : TomlSyntax.FALSE_VALUE; + } + + public class TomlDateTime : TomlNode, IFormattable + { + public int SecondsPrecision { get; set; } + public override bool HasValue { get; } = true; + public virtual string ToString(string format, IFormatProvider formatProvider) => string.Empty; + public virtual string ToString(IFormatProvider formatProvider) => string.Empty; + protected virtual string ToInlineTomlInternal() => string.Empty; + + public override string ToInlineToml() => ToInlineTomlInternal() + .Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator) + .Replace(TomlSyntax.ISO861ZeroZone, TomlSyntax.RFC3339ZeroZone); + } + + public class TomlDateTimeOffset : TomlDateTime + { + public override bool IsDateTimeOffset { get; } = true; + public DateTimeOffset Value { get; set; } + + public override string ToString() => Value.ToString(CultureInfo.CurrentCulture); + public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); + + public override string ToString(string format, IFormatProvider formatProvider) => + Value.ToString(format, formatProvider); + + protected override string ToInlineTomlInternal() => Value.ToString(TomlSyntax.RFC3339Formats[SecondsPrecision]); + } + + public class TomlDateTimeLocal : TomlDateTime + { + public enum DateTimeStyle + { + Date, + Time, + DateTime + } + + public override bool IsDateTimeLocal { get; } = true; + public DateTimeStyle Style { get; set; } = DateTimeStyle.DateTime; + public DateTime Value { get; set; } + + public override string ToString() => Value.ToString(CultureInfo.CurrentCulture); + + public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); + + public override string ToString(string format, IFormatProvider formatProvider) => + Value.ToString(format, formatProvider); + + public override string ToInlineToml() => + Style switch + { + DateTimeStyle.Date => Value.ToString(TomlSyntax.LocalDateFormat), + DateTimeStyle.Time => Value.ToString(TomlSyntax.RFC3339LocalTimeFormats[SecondsPrecision]), + var _ => Value.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision]) + }; + } + + public class TomlArray : TomlNode + { + private List values; + + public override bool HasValue { get; } = true; + public override bool IsArray { get; } = true; + public bool IsMultiline { get; set; } + public bool IsTableArray { get; set; } + public List RawArray => values ??= new List(); + + public override TomlNode this[int index] + { + get + { + if (index < RawArray.Count) return RawArray[index]; + var lazy = new TomlLazy(this); + this[index] = lazy; + return lazy; + } + set + { + if (index == RawArray.Count) + RawArray.Add(value); + else + RawArray[index] = value; + } + } + + public override int ChildrenCount => RawArray.Count; + + public override IEnumerable Children => RawArray.AsEnumerable(); + + public override void Add(TomlNode node) => RawArray.Add(node); + + public override void AddRange(IEnumerable nodes) => RawArray.AddRange(nodes); + + public override void Delete(TomlNode node) => RawArray.Remove(node); + + public override void Delete(int index) => RawArray.RemoveAt(index); + + public override string ToString() => ToString(false); + + public string ToString(bool multiline) + { + var sb = new StringBuilder(); + sb.Append(TomlSyntax.ARRAY_START_SYMBOL); + if (ChildrenCount != 0) + { + var arrayStart = multiline ? $"{Environment.NewLine} " : " "; + var arraySeparator = multiline ? $"{TomlSyntax.ITEM_SEPARATOR}{Environment.NewLine} " : $"{TomlSyntax.ITEM_SEPARATOR} "; + var arrayEnd = multiline ? Environment.NewLine : " "; + sb.Append(arrayStart) + .Append(arraySeparator.Join(RawArray.Select(n => n.ToInlineToml()))) + .Append(arrayEnd); + } + sb.Append(TomlSyntax.ARRAY_END_SYMBOL); + return sb.ToString(); + } + + public override void WriteTo(TextWriter tw, string name = null) + { + // If it's a normal array, write it as usual + if (!IsTableArray) + { + tw.WriteLine(ToString(IsMultiline)); + return; + } + + if (!(Comment is null)) + { + tw.WriteLine(); + Comment.AsComment(tw); + } + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(name); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.WriteLine(); + + var first = true; + + foreach (var tomlNode in RawArray) + { + if (!(tomlNode is TomlTable tbl)) + throw new TomlFormatException("The array is marked as array table but contains non-table nodes!"); + + // Ensure it's parsed as a section + tbl.IsInline = false; + + if (!first) + { + tw.WriteLine(); + + Comment?.AsComment(tw); + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(name); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.WriteLine(); + } + + first = false; + + // Don't write section since it's already written here + tbl.WriteTo(tw, name, false); + } + } + } + + public class TomlTable : TomlNode + { + private Dictionary children; + internal bool isImplicit; + + public override bool HasValue { get; } = false; + public override bool IsTable { get; } = true; + public bool IsInline { get; set; } + public Dictionary RawTable => children ??= new Dictionary(); + + public override TomlNode this[string key] + { + get + { + if (RawTable.TryGetValue(key, out var result)) return result; + var lazy = new TomlLazy(this); + RawTable[key] = lazy; + return lazy; + } + set => RawTable[key] = value; + } + + public override int ChildrenCount => RawTable.Count; + public override IEnumerable Children => RawTable.Select(kv => kv.Value); + public override IEnumerable Keys => RawTable.Select(kv => kv.Key); + public override bool HasKey(string key) => RawTable.ContainsKey(key); + public override void Add(string key, TomlNode node) => RawTable.Add(key, node); + public override bool TryGetNode(string key, out TomlNode node) => RawTable.TryGetValue(key, out node); + public override void Delete(TomlNode node) => RawTable.Remove(RawTable.First(kv => kv.Value == node).Key); + public override void Delete(string key) => RawTable.Remove(key); + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(TomlSyntax.INLINE_TABLE_START_SYMBOL); + + if (ChildrenCount != 0) + { + var collapsed = CollectCollapsedItems(normalizeOrder: false); + + if (collapsed.Count != 0) + sb.Append(' ') + .Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(collapsed.Select(n => + $"{n.Key} {TomlSyntax.KEY_VALUE_SEPARATOR} {n.Value.ToInlineToml()}"))); + sb.Append(' '); + } + + sb.Append(TomlSyntax.INLINE_TABLE_END_SYMBOL); + return sb.ToString(); + } + + private LinkedList> CollectCollapsedItems(string prefix = "", int level = 0, bool normalizeOrder = true) + { + var nodes = new LinkedList>(); + var postNodes = normalizeOrder ? new LinkedList>() : nodes; + + foreach (var keyValuePair in RawTable) + { + var node = keyValuePair.Value; + var key = keyValuePair.Key.AsKey(); + + if (node is TomlTable tbl) + { + var subnodes = tbl.CollectCollapsedItems($"{prefix}{key}.", level + 1, normalizeOrder); + // Write main table first before writing collapsed items + if (subnodes.Count == 0 && node.CollapseLevel == level) + { + postNodes.AddLast(new KeyValuePair($"{prefix}{key}", node)); + } + foreach (var kv in subnodes) + postNodes.AddLast(kv); + } + else if (node.CollapseLevel == level) + nodes.AddLast(new KeyValuePair($"{prefix}{key}", node)); + } + + if (normalizeOrder) + foreach (var kv in postNodes) + nodes.AddLast(kv); + + return nodes; + } + + public override void WriteTo(TextWriter tw, string name = null) => WriteTo(tw, name, true); + + internal void WriteTo(TextWriter tw, string name, bool writeSectionName) + { + // The table is inline table + if (IsInline && name != null) + { + tw.WriteLine(ToInlineToml()); + return; + } + + var collapsedItems = CollectCollapsedItems(); + + if (collapsedItems.Count == 0) + return; + + var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } or TomlArray { IsTableArray: true }); + + Comment?.AsComment(tw); + + if (name != null && (hasRealValues || Comment != null) && writeSectionName) + { + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(name); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.WriteLine(); + } + else if (Comment != null) // Add some spacing between the first node and the comment + { + tw.WriteLine(); + } + + var namePrefix = name == null ? "" : $"{name}."; + var first = true; + + foreach (var collapsedItem in collapsedItems) + { + var key = collapsedItem.Key; + if (collapsedItem.Value is TomlArray { IsTableArray: true } or TomlTable { IsInline: false }) + { + if (!first) tw.WriteLine(); + first = false; + collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}"); + continue; + } + first = false; + + collapsedItem.Value.Comment?.AsComment(tw); + tw.Write(key); + tw.Write(' '); + tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR); + tw.Write(' '); + + collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}"); + } + } + } + + internal class TomlLazy : TomlNode + { + private readonly TomlNode parent; + private TomlNode replacement; + + public TomlLazy(TomlNode parent) => this.parent = parent; + + public override TomlNode this[int index] + { + get => Set()[index]; + set => Set()[index] = value; + } + + public override TomlNode this[string key] + { + get => Set()[key]; + set => Set()[key] = value; + } + + public override void Add(TomlNode node) => Set().Add(node); + + public override void Add(string key, TomlNode node) => Set().Add(key, node); + + public override void AddRange(IEnumerable nodes) => Set().AddRange(nodes); + + private TomlNode Set() where T : TomlNode, new() + { + if (replacement != null) return replacement; + + var newNode = new T + { + Comment = Comment + }; + + if (parent.IsTable) + { + var key = parent.Keys.FirstOrDefault(s => parent.TryGetNode(s, out var node) && node.Equals(this)); + if (key == null) return default(T); + + parent[key] = newNode; + } + else if (parent.IsArray) + { + var index = parent.Children.TakeWhile(child => child != this).Count(); + if (index == parent.ChildrenCount) return default(T); + parent[index] = newNode; + } + else + { + return default(T); + } + + replacement = newNode; + return newNode; + } + } + + #endregion + + #region Parser + + public class TOMLParser : IDisposable + { + public enum ParseState + { + None, + KeyValuePair, + SkipToNextLine, + Table + } + + private readonly TextReader reader; + private ParseState currentState; + private int line, col; + private List syntaxErrors; + + public TOMLParser(TextReader reader) + { + this.reader = reader; + line = col = 0; + } + + public bool ForceASCII { get; set; } + + public void Dispose() => reader?.Dispose(); + + public TomlTable Parse() + { + syntaxErrors = new List(); + line = col = 1; + var rootNode = new TomlTable(); + var currentNode = rootNode; + currentState = ParseState.None; + var keyParts = new List(); + var arrayTable = false; + StringBuilder latestComment = null; + var firstComment = true; + + int currentChar; + while ((currentChar = reader.Peek()) >= 0) + { + var c = (char)currentChar; + + if (currentState == ParseState.None) + { + // Skip white space + if (TomlSyntax.IsWhiteSpace(c)) goto consume_character; + + if (TomlSyntax.IsNewLine(c)) + { + // Check if there are any comments and so far no items being declared + if (latestComment != null && firstComment) + { + rootNode.Comment = latestComment.ToString().TrimEnd(); + latestComment = null; + firstComment = false; + } + + if (TomlSyntax.IsLineBreak(c)) + AdvanceLine(); + + goto consume_character; + } + + // Start of a comment; ignore until newline + if (c == TomlSyntax.COMMENT_SYMBOL) + { + latestComment ??= new StringBuilder(); + latestComment.AppendLine(ParseComment()); + AdvanceLine(1); + continue; + } + + // Encountered a non-comment value. The comment must belong to it (ignore possible newlines)! + firstComment = false; + + if (c == TomlSyntax.TABLE_START_SYMBOL) + { + currentState = ParseState.Table; + goto consume_character; + } + + if (TomlSyntax.IsBareKey(c) || TomlSyntax.IsQuoted(c)) + { + currentState = ParseState.KeyValuePair; + } + else + { + AddError($"Unexpected character \"{c}\""); + continue; + } + } + + if (currentState == ParseState.KeyValuePair) + { + var keyValuePair = ReadKeyValuePair(keyParts); + + if (keyValuePair == null) + { + latestComment = null; + keyParts.Clear(); + + if (currentState != ParseState.None) + AddError("Failed to parse key-value pair!"); + continue; + } + + keyValuePair.Comment = latestComment?.ToString()?.TrimEnd(); + var inserted = InsertNode(keyValuePair, currentNode, keyParts); + latestComment = null; + keyParts.Clear(); + if (inserted) + currentState = ParseState.SkipToNextLine; + continue; + } + + if (currentState == ParseState.Table) + { + if (keyParts.Count == 0) + { + // We have array table + if (c == TomlSyntax.TABLE_START_SYMBOL) + { + // Consume the character + ConsumeChar(); + arrayTable = true; + } + + if (!ReadKeyName(ref keyParts, TomlSyntax.TABLE_END_SYMBOL)) + { + keyParts.Clear(); + continue; + } + + if (keyParts.Count == 0) + { + AddError("Table name is emtpy."); + arrayTable = false; + latestComment = null; + keyParts.Clear(); + } + + continue; + } + + if (c == TomlSyntax.TABLE_END_SYMBOL) + { + if (arrayTable) + { + // Consume the ending bracket so we can peek the next character + ConsumeChar(); + var nextChar = reader.Peek(); + if (nextChar < 0 || (char)nextChar != TomlSyntax.TABLE_END_SYMBOL) + { + AddError($"Array table {".".Join(keyParts)} has only one closing bracket."); + keyParts.Clear(); + arrayTable = false; + latestComment = null; + continue; + } + } + + currentNode = CreateTable(rootNode, keyParts, arrayTable); + if (currentNode != null) + { + currentNode.IsInline = false; + currentNode.Comment = latestComment?.ToString()?.TrimEnd(); + } + + keyParts.Clear(); + arrayTable = false; + latestComment = null; + + if (currentNode == null) + { + if (currentState != ParseState.None) + AddError("Error creating table array!"); + // Reset a node to root in order to try and continue parsing + currentNode = rootNode; + continue; + } + + currentState = ParseState.SkipToNextLine; + goto consume_character; + } + + if (keyParts.Count != 0) + { + AddError($"Unexpected character \"{c}\""); + keyParts.Clear(); + arrayTable = false; + latestComment = null; + } + } + + if (currentState == ParseState.SkipToNextLine) + { + if (TomlSyntax.IsWhiteSpace(c) || c == TomlSyntax.NEWLINE_CARRIAGE_RETURN_CHARACTER) + goto consume_character; + + if (c is TomlSyntax.COMMENT_SYMBOL or TomlSyntax.NEWLINE_CHARACTER) + { + currentState = ParseState.None; + AdvanceLine(); + + if (c == TomlSyntax.COMMENT_SYMBOL) + { + col++; + ParseComment(); + continue; + } + + goto consume_character; + } + + AddError($"Unexpected character \"{c}\" at the end of the line."); + } + + consume_character: + reader.Read(); + col++; + } + + if (currentState != ParseState.None && currentState != ParseState.SkipToNextLine) + AddError("Unexpected end of file!"); + + if (syntaxErrors.Count > 0) + throw new TomlParseException(rootNode, syntaxErrors); + + return rootNode; + } + + private bool AddError(string message, bool skipLine = true) + { + syntaxErrors.Add(new TomlSyntaxException(message, currentState, line, col)); + // Skip the whole line in hope that it was only a single faulty value (and non-multiline one at that) + if (skipLine) + { + reader.ReadLine(); + AdvanceLine(1); + } + currentState = ParseState.None; + return false; + } + + private void AdvanceLine(int startCol = 0) + { + line++; + col = startCol; + } + + private int ConsumeChar() + { + col++; + return reader.Read(); + } + + #region Key-Value pair parsing + + /** + * Reads a single key-value pair. + * Assumes the cursor is at the first character that belong to the pair (including possible whitespace). + * Consumes all characters that belong to the key and the value (ignoring possible trailing whitespace at the end). + * + * Example: + * foo = "bar" ==> foo = "bar" + * ^ ^ + */ + private TomlNode ReadKeyValuePair(List keyParts) + { + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (TomlSyntax.IsQuoted(c) || TomlSyntax.IsBareKey(c)) + { + if (keyParts.Count != 0) + { + AddError("Encountered extra characters in key definition!"); + return null; + } + + if (!ReadKeyName(ref keyParts, TomlSyntax.KEY_VALUE_SEPARATOR)) + return null; + + continue; + } + + if (TomlSyntax.IsWhiteSpace(c)) + { + ConsumeChar(); + continue; + } + + if (c == TomlSyntax.KEY_VALUE_SEPARATOR) + { + ConsumeChar(); + return ReadValue(); + } + + AddError($"Unexpected character \"{c}\" in key name."); + return null; + } + + return null; + } + + /** + * Reads a single value. + * Assumes the cursor is at the first character that belongs to the value (including possible starting whitespace). + * Consumes all characters belonging to the value (ignoring possible trailing whitespace at the end). + * + * Example: + * "test" ==> "test" + * ^ ^ + */ + private TomlNode ReadValue(bool skipNewlines = false) + { + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (TomlSyntax.IsWhiteSpace(c)) + { + ConsumeChar(); + continue; + } + + if (c == TomlSyntax.COMMENT_SYMBOL) + { + AddError("No value found!"); + return null; + } + + if (TomlSyntax.IsNewLine(c)) + { + if (skipNewlines) + { + reader.Read(); + AdvanceLine(1); + continue; + } + + AddError("Encountered a newline when expecting a value!"); + return null; + } + + if (TomlSyntax.IsQuoted(c)) + { + var isMultiline = IsTripleQuote(c, out var excess); + + // Error occurred in triple quote parsing + if (currentState == ParseState.None) + return null; + + var value = isMultiline + ? ReadQuotedValueMultiLine(c) + : ReadQuotedValueSingleLine(c, excess); + + if (value is null) + return null; + + return new TomlString + { + Value = value, + IsMultiline = isMultiline, + PreferLiteral = c == TomlSyntax.LITERAL_STRING_SYMBOL + }; + } + + return c switch + { + TomlSyntax.INLINE_TABLE_START_SYMBOL => ReadInlineTable(), + TomlSyntax.ARRAY_START_SYMBOL => ReadArray(), + var _ => ReadTomlValue() + }; + } + + return null; + } + + /** + * Reads a single key name. + * Assumes the cursor is at the first character belonging to the key (with possible trailing whitespace if `skipWhitespace = true`). + * Consumes all the characters until the `until` character is met (but does not consume the character itself). + * + * Example 1: + * foo.bar ==> foo.bar (`skipWhitespace = false`, `until = ' '`) + * ^ ^ + * + * Example 2: + * [ foo . bar ] ==> [ foo . bar ] (`skipWhitespace = true`, `until = ']'`) + * ^ ^ + */ + private bool ReadKeyName(ref List parts, char until) + { + var buffer = new StringBuilder(); + var quoted = false; + var prevWasSpace = false; + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + // Reached the final character + if (c == until) break; + + if (TomlSyntax.IsWhiteSpace(c)) + { + prevWasSpace = true; + goto consume_character; + } + + if (buffer.Length == 0) prevWasSpace = false; + + if (c == TomlSyntax.SUBKEY_SEPARATOR) + { + if (buffer.Length == 0 && !quoted) + return AddError($"Found an extra subkey separator in {".".Join(parts)}..."); + + parts.Add(buffer.ToString()); + buffer.Length = 0; + quoted = false; + prevWasSpace = false; + goto consume_character; + } + + if (prevWasSpace) + return AddError("Invalid spacing in key name"); + + if (TomlSyntax.IsQuoted(c)) + { + if (quoted) + + return AddError("Expected a subkey separator but got extra data instead!"); + + if (buffer.Length != 0) + return AddError("Encountered a quote in the middle of subkey name!"); + + // Consume the quote character and read the key name + col++; + buffer.Append(ReadQuotedValueSingleLine((char)reader.Read())); + quoted = true; + continue; + } + + if (TomlSyntax.IsBareKey(c)) + { + buffer.Append(c); + goto consume_character; + } + + // If we see an invalid symbol, let the next parser handle it + break; + + consume_character: + reader.Read(); + col++; + } + + if (buffer.Length == 0 && !quoted) + return AddError($"Found an extra subkey separator in {".".Join(parts)}..."); + + parts.Add(buffer.ToString()); + + return true; + } + + #endregion + + #region Non-string value parsing + + /** + * Reads the whole raw value until the first non-value character is encountered. + * Assumes the cursor start position at the first value character and consumes all characters that may be related to the value. + * Example: + * + * 1_0_0_0 ==> 1_0_0_0 + * ^ ^ + */ + private string ReadRawValue() + { + var result = new StringBuilder(); + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + if (c == TomlSyntax.COMMENT_SYMBOL || TomlSyntax.IsNewLine(c) || TomlSyntax.IsValueSeparator(c)) break; + result.Append(c); + ConsumeChar(); + } + + // Replace trim with manual space counting? + return result.ToString().Trim(); + } + + /** + * Reads and parses a non-string, non-composite TOML value. + * Assumes the cursor at the first character that is related to the value (with possible spaces). + * Consumes all the characters that are related to the value. + * + * Example + * 1_0_0_0 # This is a comment + * + * ==> 1_0_0_0 # This is a comment + * ^ ^ + */ + private TomlNode ReadTomlValue() + { + var value = ReadRawValue(); + TomlNode node = value switch + { + var v when TomlSyntax.IsBoolean(v) => bool.Parse(v), + var v when TomlSyntax.IsNaN(v) => double.NaN, + var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity, + var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity, + var v when TomlSyntax.IsInteger(v) => long.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), + CultureInfo.InvariantCulture), + var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), + CultureInfo.InvariantCulture), + var v when TomlSyntax.IsIntegerWithBase(v, out var numberBase) => new TomlInteger + { + Value = Convert.ToInt64(value.Substring(2).RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), numberBase), + IntegerBase = (TomlInteger.Base)numberBase + }, + var _ => null + }; + if (node != null) return node; + + // Normalize by removing space separator + value = value.Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator); + if (StringUtils.TryParseDateTime(value, + TomlSyntax.RFC3339LocalDateTimeFormats, + DateTimeStyles.AssumeLocal, + DateTime.TryParseExact, + out var dateTimeResult, + out var precision)) + return new TomlDateTimeLocal + { + Value = dateTimeResult, + SecondsPrecision = precision + }; + + if (DateTime.TryParseExact(value, + TomlSyntax.LocalDateFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out dateTimeResult)) + return new TomlDateTimeLocal + { + Value = dateTimeResult, + Style = TomlDateTimeLocal.DateTimeStyle.Date + }; + + if (StringUtils.TryParseDateTime(value, + TomlSyntax.RFC3339LocalTimeFormats, + DateTimeStyles.AssumeLocal, + DateTime.TryParseExact, + out dateTimeResult, + out precision)) + return new TomlDateTimeLocal + { + Value = dateTimeResult, + Style = TomlDateTimeLocal.DateTimeStyle.Time, + SecondsPrecision = precision + }; + + if (StringUtils.TryParseDateTime(value, + TomlSyntax.RFC3339Formats, + DateTimeStyles.None, + DateTimeOffset.TryParseExact, + out var dateTimeOffsetResult, + out precision)) + return new TomlDateTimeOffset + { + Value = dateTimeOffsetResult, + SecondsPrecision = precision + }; + + AddError($"Value \"{value}\" is not a valid TOML value!"); + return null; + } + + /** + * Reads an array value. + * Assumes the cursor is at the start of the array definition. Reads all character until the array closing bracket. + * + * Example: + * [1, 2, 3] ==> [1, 2, 3] + * ^ ^ + */ + private TomlArray ReadArray() + { + // Consume the start of array character + ConsumeChar(); + var result = new TomlArray(); + TomlNode currentValue = null; + var expectValue = true; + + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (c == TomlSyntax.ARRAY_END_SYMBOL) + { + ConsumeChar(); + break; + } + + if (c == TomlSyntax.COMMENT_SYMBOL) + { + reader.ReadLine(); + AdvanceLine(1); + continue; + } + + if (TomlSyntax.IsWhiteSpace(c) || TomlSyntax.IsNewLine(c)) + { + if (TomlSyntax.IsLineBreak(c)) + AdvanceLine(); + goto consume_character; + } + + if (c == TomlSyntax.ITEM_SEPARATOR) + { + if (currentValue == null) + { + AddError("Encountered multiple value separators"); + return null; + } + + result.Add(currentValue); + currentValue = null; + expectValue = true; + goto consume_character; + } + + if (!expectValue) + { + AddError("Missing separator between values"); + return null; + } + currentValue = ReadValue(true); + if (currentValue == null) + { + if (currentState != ParseState.None) + AddError("Failed to determine and parse a value!"); + return null; + } + expectValue = false; + + continue; + consume_character: + ConsumeChar(); + } + + if (currentValue != null) result.Add(currentValue); + return result; + } + + /** + * Reads an inline table. + * Assumes the cursor is at the start of the table definition. Reads all character until the table closing bracket. + * + * Example: + * { test = "foo", value = 1 } ==> { test = "foo", value = 1 } + * ^ ^ + */ + private TomlNode ReadInlineTable() + { + ConsumeChar(); + var result = new TomlTable { IsInline = true }; + TomlNode currentValue = null; + var separator = false; + var keyParts = new List(); + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (c == TomlSyntax.INLINE_TABLE_END_SYMBOL) + { + ConsumeChar(); + break; + } + + if (c == TomlSyntax.COMMENT_SYMBOL) + { + AddError("Incomplete inline table definition!"); + return null; + } + + if (TomlSyntax.IsNewLine(c)) + { + AddError("Inline tables are only allowed to be on single line"); + return null; + } + + if (TomlSyntax.IsWhiteSpace(c)) + goto consume_character; + + if (c == TomlSyntax.ITEM_SEPARATOR) + { + if (currentValue == null) + { + AddError("Encountered multiple value separators in inline table!"); + return null; + } + + if (!InsertNode(currentValue, result, keyParts)) + return null; + keyParts.Clear(); + currentValue = null; + separator = true; + goto consume_character; + } + + separator = false; + currentValue = ReadKeyValuePair(keyParts); + continue; + + consume_character: + ConsumeChar(); + } + + if (separator) + { + AddError("Trailing commas are not allowed in inline tables."); + return null; + } + + if (currentValue != null && !InsertNode(currentValue, result, keyParts)) + return null; + + return result; + } + + #endregion + + #region String parsing + + /** + * Checks if the string value a multiline string (i.e. a triple quoted string). + * Assumes the cursor is at the first quote character. Consumes the least amount of characters needed to determine if the string is multiline. + * + * If the result is false, returns the consumed character through the `excess` variable. + * + * Example 1: + * """test""" ==> """test""" + * ^ ^ + * + * Example 2: + * "test" ==> "test" (doesn't return the first quote) + * ^ ^ + * + * Example 3: + * "" ==> "" (returns the extra `"` through the `excess` variable) + * ^ ^ + */ + private bool IsTripleQuote(char quote, out char excess) + { + // Copypasta, but it's faster... + + int cur; + // Consume the first quote + ConsumeChar(); + if ((cur = reader.Peek()) < 0) + { + excess = '\0'; + return AddError("Unexpected end of file!"); + } + + if ((char)cur != quote) + { + excess = '\0'; + return false; + } + + // Consume the second quote + excess = (char)ConsumeChar(); + if ((cur = reader.Peek()) < 0 || (char)cur != quote) return false; + + // Consume the final quote + ConsumeChar(); + excess = '\0'; + return true; + } + + /** + * A convenience method to process a single character within a quote. + */ + private bool ProcessQuotedValueCharacter(char quote, + bool isNonLiteral, + char c, + StringBuilder sb, + ref bool escaped) + { + if (TomlSyntax.MustBeEscaped(c)) + return AddError($"The character U+{(int)c:X8} must be escaped in a string!"); + + if (escaped) + { + sb.Append(c); + escaped = false; + return false; + } + + if (c == quote) + { + if (!isNonLiteral && reader.Peek() == quote) + { + reader.Read(); + col++; + sb.Append(quote); + return false; + } + + return true; + } + if (isNonLiteral && c == TomlSyntax.ESCAPE_SYMBOL) + escaped = true; + if (c == TomlSyntax.NEWLINE_CHARACTER) + return AddError("Encountered newline in single line string!"); + + sb.Append(c); + return false; + } + + /** + * Reads a single-line string. + * Assumes the cursor is at the first character that belongs to the string. + * Consumes all characters that belong to the string (including the closing quote). + * + * Example: + * "test" ==> "test" + * ^ ^ + */ + private string ReadQuotedValueSingleLine(char quote, char initialData = '\0') + { + var isNonLiteral = quote == TomlSyntax.BASIC_STRING_SYMBOL; + var sb = new StringBuilder(); + var escaped = false; + + if (initialData != '\0') + { + var shouldReturn = + ProcessQuotedValueCharacter(quote, isNonLiteral, initialData, sb, ref escaped); + if (currentState == ParseState.None) return null; + if (shouldReturn) + if (isNonLiteral) + { + if (sb.ToString().TryUnescape(out var res, out var ex)) return res; + AddError(ex.Message); + return null; + } + else + return sb.ToString(); + } + + int cur; + var readDone = false; + while ((cur = reader.Read()) >= 0) + { + // Consume the character + col++; + var c = (char)cur; + readDone = ProcessQuotedValueCharacter(quote, isNonLiteral, c, sb, ref escaped); + if (readDone) + { + if (currentState == ParseState.None) return null; + break; + } + } + + if (!readDone) + { + AddError("Unclosed string."); + return null; + } + + if (!isNonLiteral) return sb.ToString(); + if (sb.ToString().TryUnescape(out var unescaped, out var unescapedEx)) return unescaped; + AddError(unescapedEx.Message); + return null; + } + + /** + * Reads a multiline string. + * Assumes the cursor is at the first character that belongs to the string. + * Consumes all characters that belong to the string and the three closing quotes. + * + * Example: + * """test""" ==> """test""" + * ^ ^ + */ + private string ReadQuotedValueMultiLine(char quote) + { + var isBasic = quote == TomlSyntax.BASIC_STRING_SYMBOL; + var sb = new StringBuilder(); + var escaped = false; + var skipWhitespace = false; + var skipWhitespaceLineSkipped = false; + var quotesEncountered = 0; + var first = true; + int cur; + while ((cur = ConsumeChar()) >= 0) + { + var c = (char)cur; + if (TomlSyntax.MustBeEscaped(c, true)) + { + AddError($"The character U+{(int)c:X8} must be escaped!"); + return null; + } + // Trim the first newline + if (first && TomlSyntax.IsNewLine(c)) + { + if (TomlSyntax.IsLineBreak(c)) + first = false; + else + AdvanceLine(); + continue; + } + + first = false; + //TODO: Reuse ProcessQuotedValueCharacter + // Skip the current character if it is going to be escaped later + if (escaped) + { + sb.Append(c); + escaped = false; + continue; + } + + // If we are currently skipping empty spaces, skip + if (skipWhitespace) + { + if (TomlSyntax.IsEmptySpace(c)) + { + if (TomlSyntax.IsLineBreak(c)) + { + skipWhitespaceLineSkipped = true; + AdvanceLine(); + } + continue; + } + + if (!skipWhitespaceLineSkipped) + { + AddError("Non-whitespace character after trim marker."); + return null; + } + + skipWhitespaceLineSkipped = false; + skipWhitespace = false; + } + + // If we encounter an escape sequence... + if (isBasic && c == TomlSyntax.ESCAPE_SYMBOL) + { + var next = reader.Peek(); + var nc = (char)next; + if (next >= 0) + { + // ...and the next char is empty space, we must skip all whitespaces + if (TomlSyntax.IsEmptySpace(nc)) + { + skipWhitespace = true; + continue; + } + + // ...and we have \" or \, skip the character + if (nc == quote || nc == TomlSyntax.ESCAPE_SYMBOL) escaped = true; + } + } + + // Count the consecutive quotes + if (c == quote) + quotesEncountered++; + else + quotesEncountered = 0; + + // If the are three quotes, count them as closing quotes + if (quotesEncountered == 3) break; + + sb.Append(c); + } + + // TOML actually allows to have five ending quotes like + // """"" => "" belong to the string + """ is the actual ending + quotesEncountered = 0; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + if (c == quote && ++quotesEncountered < 3) + { + sb.Append(c); + ConsumeChar(); + } + else break; + } + + // Remove last two quotes (third one wasn't included by default) + sb.Length -= 2; + if (!isBasic) return sb.ToString(); + if (sb.ToString().TryUnescape(out var res, out var ex)) return res; + AddError(ex.Message); + return null; + } + + #endregion + + #region Node creation + + private bool InsertNode(TomlNode node, TomlNode root, IList path) + { + var latestNode = root; + if (path.Count > 1) + for (var index = 0; index < path.Count - 1; index++) + { + var subkey = path[index]; + if (latestNode.TryGetNode(subkey, out var currentNode)) + { + if (currentNode.HasValue) + return AddError($"The key {".".Join(path)} already has a value assigned to it!"); + } + else + { + currentNode = new TomlTable(); + latestNode[subkey] = currentNode; + } + + latestNode = currentNode; + if (latestNode is TomlTable { IsInline: true }) + return AddError($"Cannot assign {".".Join(path)} because it will edit an immutable table."); + } + + if (latestNode.HasKey(path[path.Count - 1])) + return AddError($"The key {".".Join(path)} is already defined!"); + latestNode[path[path.Count - 1]] = node; + node.CollapseLevel = path.Count - 1; + return true; + } + + private TomlTable CreateTable(TomlNode root, IList path, bool arrayTable) + { + if (path.Count == 0) return null; + var latestNode = root; + for (var index = 0; index < path.Count; index++) + { + var subkey = path[index]; + + if (latestNode.TryGetNode(subkey, out var node)) + { + if (node.IsArray && arrayTable) + { + var arr = (TomlArray)node; + + if (!arr.IsTableArray) + { + AddError($"The array {".".Join(path)} cannot be redefined as an array table!"); + return null; + } + + if (index == path.Count - 1) + { + latestNode = new TomlTable(); + arr.Add(latestNode); + break; + } + + latestNode = arr[arr.ChildrenCount - 1]; + continue; + } + + if (node is TomlTable { IsInline: true }) + { + AddError($"Cannot create table {".".Join(path)} because it will edit an immutable table."); + return null; + } + + if (node.HasValue) + { + if (!(node is TomlArray { IsTableArray: true } array)) + { + AddError($"The key {".".Join(path)} has a value assigned to it!"); + return null; + } + + latestNode = array[array.ChildrenCount - 1]; + continue; + } + + if (index == path.Count - 1) + { + if (arrayTable && !node.IsArray) + { + AddError($"The table {".".Join(path)} cannot be redefined as an array table!"); + return null; + } + + if (node is TomlTable { isImplicit: false }) + { + AddError($"The table {".".Join(path)} is defined multiple times!"); + return null; + } + } + } + else + { + if (index == path.Count - 1 && arrayTable) + { + var table = new TomlTable(); + var arr = new TomlArray + { + IsTableArray = true + }; + arr.Add(table); + latestNode[subkey] = arr; + latestNode = table; + break; + } + + node = new TomlTable { isImplicit = true }; + latestNode[subkey] = node; + } + + latestNode = node; + } + + var result = (TomlTable)latestNode; + result.isImplicit = false; + return result; + } + + #endregion + + #region Misc parsing + + private string ParseComment() + { + ConsumeChar(); + var commentLine = reader.ReadLine()?.Trim() ?? ""; + if (commentLine.Any(ch => TomlSyntax.MustBeEscaped(ch))) + AddError("Comment must not contain control characters other than tab.", false); + return commentLine; + } + #endregion + } + + #endregion + + public static class TOML + { + public static bool ForceASCII { get; set; } = false; + + public static TomlTable Parse(TextReader reader) + { + using var parser = new TOMLParser(reader) { ForceASCII = ForceASCII }; + return parser.Parse(); + } + } + + #region Exception Types + + public class TomlFormatException : Exception + { + public TomlFormatException(string message) : base(message) { } + } + + public class TomlParseException : Exception + { + public TomlParseException(TomlTable parsed, IEnumerable exceptions) : + base("TOML file contains format errors") + { + ParsedTable = parsed; + SyntaxErrors = exceptions; + } + + public TomlTable ParsedTable { get; } + + public IEnumerable SyntaxErrors { get; } + } + + public class TomlSyntaxException : Exception + { + public TomlSyntaxException(string message, TOMLParser.ParseState state, int line, int col) : base(message) + { + ParseState = state; + Line = line; + Column = col; + } + + public TOMLParser.ParseState ParseState { get; } + + public int Line { get; } + + public int Column { get; } + } + + #endregion + + #region Parse utilities + + internal static class TomlSyntax + { + #region Type Patterns + + public const string TRUE_VALUE = "true"; + public const string FALSE_VALUE = "false"; + public const string NAN_VALUE = "nan"; + public const string POS_NAN_VALUE = "+nan"; + public const string NEG_NAN_VALUE = "-nan"; + public const string INF_VALUE = "inf"; + public const string POS_INF_VALUE = "+inf"; + public const string NEG_INF_VALUE = "-inf"; + + public static bool IsBoolean(string s) => s is TRUE_VALUE or FALSE_VALUE; + + public static bool IsPosInf(string s) => s is INF_VALUE or POS_INF_VALUE; + + public static bool IsNegInf(string s) => s == NEG_INF_VALUE; + + public static bool IsNaN(string s) => s is NAN_VALUE or POS_NAN_VALUE or NEG_NAN_VALUE; + + public static bool IsInteger(string s) => IntegerPattern.IsMatch(s); + + public static bool IsFloat(string s) => FloatPattern.IsMatch(s); + + public static bool IsIntegerWithBase(string s, out int numberBase) + { + numberBase = 10; + var match = BasedIntegerPattern.Match(s); + if (!match.Success) return false; + IntegerBases.TryGetValue(match.Groups["base"].Value, out numberBase); + return true; + } + + /** + * A pattern to verify the integer value according to the TOML specification. + */ + public static readonly Regex IntegerPattern = + new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)*)$", RegexOptions.Compiled); + + /** + * A pattern to verify a special 0x, 0o and 0b forms of an integer according to the TOML specification. + */ + public static readonly Regex BasedIntegerPattern = + new(@"^0(?x|b|o)(?!_)(_?[0-9A-F])*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /** + * A pattern to verify the float value according to the TOML specification. + */ + public static readonly Regex FloatPattern = + new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)+)(((e(\+|-)?(?!_)(_?\d)+)?)|(\.(?!_)(_?\d)+(e(\+|-)?(?!_)(_?\d)+)?))$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /** + * A helper dictionary to map TOML base codes into the radii. + */ + public static readonly Dictionary IntegerBases = new() + { + ["x"] = 16, + ["o"] = 8, + ["b"] = 2 + }; + + /** + * A helper dictionary to map non-decimal bases to their TOML identifiers + */ + public static readonly Dictionary BaseIdentifiers = new() + { + [2] = "b", + [8] = "o", + [16] = "x" + }; + + public const string RFC3339EmptySeparator = " "; + public const string ISO861Separator = "T"; + public const string ISO861ZeroZone = "+00:00"; + public const string RFC3339ZeroZone = "Z"; + + /** + * Valid date formats with timezone as per RFC3339. + */ + public static readonly string[] RFC3339Formats = + { + "yyyy'-'MM-ddTHH':'mm':'ssK", "yyyy'-'MM-ddTHH':'mm':'ss'.'fK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffK", + "yyyy'-'MM-ddTHH':'mm':'ss'.'fffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffK", + "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffffK", + "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffffK" + }; + + /** + * Valid date formats without timezone (assumes local) as per RFC3339. + */ + public static readonly string[] RFC3339LocalDateTimeFormats = + { + "yyyy'-'MM-ddTHH':'mm':'ss", "yyyy'-'MM-ddTHH':'mm':'ss'.'f", "yyyy'-'MM-ddTHH':'mm':'ss'.'ff", + "yyyy'-'MM-ddTHH':'mm':'ss'.'fff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffff", + "yyyy'-'MM-ddTHH':'mm':'ss'.'fffff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffff", + "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffff" + }; + + /** + * Valid full date format as per TOML spec. + */ + public static readonly string LocalDateFormat = "yyyy'-'MM'-'dd"; + + /** + * Valid time formats as per TOML spec. + */ + public static readonly string[] RFC3339LocalTimeFormats = + { + "HH':'mm':'ss", "HH':'mm':'ss'.'f", "HH':'mm':'ss'.'ff", "HH':'mm':'ss'.'fff", "HH':'mm':'ss'.'ffff", + "HH':'mm':'ss'.'fffff", "HH':'mm':'ss'.'ffffff", "HH':'mm':'ss'.'fffffff" + }; + + #endregion + + #region Character definitions + + public const char ARRAY_END_SYMBOL = ']'; + public const char ITEM_SEPARATOR = ','; + public const char ARRAY_START_SYMBOL = '['; + public const char BASIC_STRING_SYMBOL = '\"'; + public const char COMMENT_SYMBOL = '#'; + public const char ESCAPE_SYMBOL = '\\'; + public const char KEY_VALUE_SEPARATOR = '='; + public const char NEWLINE_CARRIAGE_RETURN_CHARACTER = '\r'; + public const char NEWLINE_CHARACTER = '\n'; + public const char SUBKEY_SEPARATOR = '.'; + public const char TABLE_END_SYMBOL = ']'; + public const char TABLE_START_SYMBOL = '['; + public const char INLINE_TABLE_START_SYMBOL = '{'; + public const char INLINE_TABLE_END_SYMBOL = '}'; + public const char LITERAL_STRING_SYMBOL = '\''; + public const char INT_NUMBER_SEPARATOR = '_'; + + public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER }; + + public static bool IsQuoted(char c) => c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL; + + public static bool IsWhiteSpace(char c) => c is ' ' or '\t'; + + public static bool IsNewLine(char c) => c is NEWLINE_CHARACTER or NEWLINE_CARRIAGE_RETURN_CHARACTER; + + public static bool IsLineBreak(char c) => c == NEWLINE_CHARACTER; + + public static bool IsEmptySpace(char c) => IsWhiteSpace(c) || IsNewLine(c); + + public static bool IsBareKey(char c) => + c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_' or '-'; + + public static bool MustBeEscaped(char c, bool allowNewLines = false) + { + var result = c is (>= '\u0000' and <= '\u0008') or '\u000b' or '\u000c' or (>= '\u000e' and <= '\u001f') or '\u007f'; + if (!allowNewLines) + result |= c is >= '\u000a' and <= '\u000e'; + return result; + } + + public static bool IsValueSeparator(char c) => + c is ITEM_SEPARATOR or ARRAY_END_SYMBOL or INLINE_TABLE_END_SYMBOL; + + #endregion + } + + internal static class StringUtils + { + public static string AsKey(this string key) + { + var quote = key == string.Empty || key.Any(c => !TomlSyntax.IsBareKey(c)); + return !quote ? key : $"{TomlSyntax.BASIC_STRING_SYMBOL}{key.Escape()}{TomlSyntax.BASIC_STRING_SYMBOL}"; + } + + public static string Join(this string self, IEnumerable subItems) + { + var sb = new StringBuilder(); + var first = true; + + foreach (var subItem in subItems) + { + if (!first) sb.Append(self); + first = false; + sb.Append(subItem); + } + + return sb.ToString(); + } + + public delegate bool TryDateParseDelegate(string s, string format, IFormatProvider ci, DateTimeStyles dts, out T dt); + + public static bool TryParseDateTime(string s, + string[] formats, + DateTimeStyles styles, + TryDateParseDelegate parser, + out T dateTime, + out int parsedFormat) + { + parsedFormat = 0; + dateTime = default; + for (var i = 0; i < formats.Length; i++) + { + var format = formats[i]; + if (!parser(s, format, CultureInfo.InvariantCulture, styles, out dateTime)) continue; + parsedFormat = i; + return true; + } + + return false; + } + + public static void AsComment(this string self, TextWriter tw) + { + foreach (var line in self.Split(TomlSyntax.NEWLINE_CHARACTER)) + tw.WriteLine($"{TomlSyntax.COMMENT_SYMBOL} {line.Trim()}"); + } + + public static string RemoveAll(this string txt, char toRemove) + { + var sb = new StringBuilder(txt.Length); + foreach (var c in txt.Where(c => c != toRemove)) + sb.Append(c); + return sb.ToString(); + } + + public static string Escape(this string txt, bool escapeNewlines = true) + { + var stringBuilder = new StringBuilder(txt.Length + 2); + for (var i = 0; i < txt.Length; i++) + { + var c = txt[i]; + + static string CodePoint(string txt, ref int i, char c) => char.IsSurrogatePair(txt, i) + ? $"\\U{char.ConvertToUtf32(txt, i++):X8}" + : $"\\u{(ushort)c:X4}"; + + stringBuilder.Append(c switch + { + '\b' => @"\b", + '\t' => @"\t", + '\n' when escapeNewlines => @"\n", + '\f' => @"\f", + '\r' when escapeNewlines => @"\r", + '\\' => @"\\", + '\"' => @"\""", + var _ when TomlSyntax.MustBeEscaped(c, !escapeNewlines) || TOML.ForceASCII && c > sbyte.MaxValue => + CodePoint(txt, ref i, c), + var _ => c + }); + } + + return stringBuilder.ToString(); + } + + public static bool TryUnescape(this string txt, out string unescaped, out Exception exception) + { + try + { + exception = null; + unescaped = txt.Unescape(); + return true; + } + catch (Exception e) + { + exception = e; + unescaped = null; + return false; + } + } + + public static string Unescape(this string txt) + { + if (string.IsNullOrEmpty(txt)) return txt; + var stringBuilder = new StringBuilder(txt.Length); + for (var i = 0; i < txt.Length;) + { + var num = txt.IndexOf('\\', i); + var next = num + 1; + if (num < 0 || num == txt.Length - 1) num = txt.Length; + stringBuilder.Append(txt, i, num - i); + if (num >= txt.Length) break; + var c = txt[next]; + + static string CodePoint(int next, string txt, ref int num, int size) + { + if (next + size >= txt.Length) throw new Exception("Undefined escape sequence!"); + num += size; + return char.ConvertFromUtf32(Convert.ToInt32(txt.Substring(next + 1, size), 16)); + } + + stringBuilder.Append(c switch + { + 'b' => "\b", + 't' => "\t", + 'n' => "\n", + 'f' => "\f", + 'r' => "\r", + '\'' => "\'", + '\"' => "\"", + '\\' => "\\", + 'u' => CodePoint(next, txt, ref num, 4), + 'U' => CodePoint(next, txt, ref num, 8), + var _ => throw new Exception("Undefined escape sequence!") + }); + i = num + 2; + } + + return stringBuilder.ToString(); + } + } + + #endregion +} diff --git a/MCPForUnity/Editor/External/Tommy.cs.meta b/MCPForUnity/Editor/External/Tommy.cs.meta new file mode 100644 index 00000000..efcb8ff8 --- /dev/null +++ b/MCPForUnity/Editor/External/Tommy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea652131dcdaa44ca8cb35cd1191be3f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers.meta b/MCPForUnity/Editor/Helpers.meta new file mode 100644 index 00000000..c57a3420 --- /dev/null +++ b/MCPForUnity/Editor/Helpers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 94cb070dc5e15024da86150b27699ca0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs new file mode 100644 index 00000000..f03b66c7 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs @@ -0,0 +1,29 @@ +using System; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Provides common utility methods for working with Unity asset paths. + /// + public static class AssetPathUtility + { + /// + /// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/". + /// + public static string SanitizeAssetPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return path; + } + + path = path.Replace('\\', '/'); + if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + return "Assets/" + path.TrimStart('/'); + } + + return path; + } + } +} diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs.meta b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs.meta new file mode 100644 index 00000000..bd6a0c70 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs new file mode 100644 index 00000000..fceab479 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using MCPForUnity.External.Tommy; +using Newtonsoft.Json; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Codex CLI specific configuration helpers. Handles TOML snippet + /// generation and lightweight parsing so Codex can join the auto-setup + /// flow alongside JSON-based clients. + /// + public static class CodexConfigHelper + { + public static bool IsCodexConfigured(string pythonDir) + { + try + { + string basePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrEmpty(basePath)) return false; + + string configPath = Path.Combine(basePath, ".codex", "config.toml"); + if (!File.Exists(configPath)) return false; + + string toml = File.ReadAllText(configPath); + if (!TryParseCodexServer(toml, out _, out var args)) return false; + + string dir = McpConfigFileHelper.ExtractDirectoryArg(args); + if (string.IsNullOrEmpty(dir)) return false; + + return McpConfigFileHelper.PathsEqual(dir, pythonDir); + } + catch + { + return false; + } + } + + public static string BuildCodexServerBlock(string uvPath, string serverSrc) + { + string argsArray = FormatTomlStringArray(new[] { "run", "--directory", serverSrc, "server.py" }); + return $"[mcp_servers.unityMCP]{Environment.NewLine}" + + $"command = \"{EscapeTomlString(uvPath)}\"{Environment.NewLine}" + + $"args = {argsArray}"; + } + + public static string UpsertCodexServerBlock(string existingToml, string newBlock) + { + if (string.IsNullOrWhiteSpace(existingToml)) + { + return newBlock.TrimEnd() + Environment.NewLine; + } + + StringBuilder sb = new StringBuilder(); + using StringReader reader = new StringReader(existingToml); + string line; + bool inTarget = false; + bool replaced = false; + while ((line = reader.ReadLine()) != null) + { + string trimmed = line.Trim(); + bool isSection = trimmed.StartsWith("[") && trimmed.EndsWith("]") && !trimmed.StartsWith("[["); + if (isSection) + { + bool isTarget = string.Equals(trimmed, "[mcp_servers.unityMCP]", StringComparison.OrdinalIgnoreCase); + if (isTarget) + { + if (!replaced) + { + if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine(); + sb.AppendLine(newBlock.TrimEnd()); + replaced = true; + } + inTarget = true; + continue; + } + + if (inTarget) + { + inTarget = false; + } + } + + if (inTarget) + { + continue; + } + + sb.AppendLine(line); + } + + if (!replaced) + { + if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine(); + sb.AppendLine(newBlock.TrimEnd()); + } + + return sb.ToString().TrimEnd() + Environment.NewLine; + } + + public static bool TryParseCodexServer(string toml, out string command, out string[] args) + { + command = null; + args = null; + if (string.IsNullOrWhiteSpace(toml)) return false; + + try + { + using var reader = new StringReader(toml); + TomlTable root = TOML.Parse(reader); + if (root == null) return false; + + if (!TryGetTable(root, "mcp_servers", out var servers) + && !TryGetTable(root, "mcpServers", out servers)) + { + return false; + } + + if (!TryGetTable(servers, "unityMCP", out var unity)) + { + return false; + } + + command = GetTomlString(unity, "command"); + args = GetTomlStringArray(unity, "args"); + + return !string.IsNullOrEmpty(command) && args != null; + } + catch (TomlParseException) + { + return false; + } + catch (TomlSyntaxException) + { + return false; + } + catch (FormatException) + { + return false; + } + } + + private static bool TryGetTable(TomlTable parent, string key, out TomlTable table) + { + table = null; + if (parent == null) return false; + + if (parent.TryGetNode(key, out var node)) + { + if (node is TomlTable tbl) + { + table = tbl; + return true; + } + + if (node is TomlArray array) + { + var firstTable = array.Children.OfType().FirstOrDefault(); + if (firstTable != null) + { + table = firstTable; + return true; + } + } + } + + return false; + } + + private static string GetTomlString(TomlTable table, string key) + { + if (table != null && table.TryGetNode(key, out var node)) + { + if (node is TomlString str) return str.Value; + if (node.HasValue) return node.ToString(); + } + return null; + } + + private static string[] GetTomlStringArray(TomlTable table, string key) + { + if (table == null) return null; + if (!table.TryGetNode(key, out var node)) return null; + + if (node is TomlArray array) + { + List values = new List(); + foreach (TomlNode element in array.Children) + { + if (element is TomlString str) + { + values.Add(str.Value); + } + else if (element.HasValue) + { + values.Add(element.ToString()); + } + } + + return values.Count > 0 ? values.ToArray() : Array.Empty(); + } + + if (node is TomlString single) + { + return new[] { single.Value }; + } + + return null; + } + + private static string FormatTomlStringArray(IEnumerable values) + { + if (values == null) return "[]"; + StringBuilder sb = new StringBuilder(); + sb.Append('['); + bool first = true; + foreach (string value in values) + { + if (!first) + { + sb.Append(", "); + } + sb.Append('"').Append(EscapeTomlString(value ?? string.Empty)).Append('"'); + first = false; + } + sb.Append(']'); + return sb.ToString(); + } + + private static string EscapeTomlString(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value + .Replace("\\", "\\\\") + .Replace("\"", "\\\""); + } + + } +} diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs.meta b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs.meta new file mode 100644 index 00000000..581a4474 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b3e68082ffc0b4cd39d3747673a4cc22 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs new file mode 100644 index 00000000..5889e4f6 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs @@ -0,0 +1,129 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Helpers +{ + public static class ConfigJsonBuilder + { + public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client) + { + var root = new JObject(); + bool isVSCode = client?.mcpType == McpTypes.VSCode; + JObject container; + if (isVSCode) + { + container = EnsureObject(root, "servers"); + } + else + { + container = EnsureObject(root, "mcpServers"); + } + + var unity = new JObject(); + PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode); + + container["unityMCP"] = unity; + + return root.ToString(Formatting.Indented); + } + + public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client) + { + if (root == null) root = new JObject(); + bool isVSCode = client?.mcpType == McpTypes.VSCode; + JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers"); + JObject unity = container["unityMCP"] as JObject ?? new JObject(); + PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode); + + container["unityMCP"] = unity; + return root; + } + + /// + /// Centralized builder that applies all caveats consistently. + /// - Sets command/args with provided directory + /// - Ensures env exists + /// - Adds type:"stdio" for VSCode + /// - Adds disabled:false for Windsurf/Kiro only when missing + /// + private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode) + { + unity["command"] = uvPath; + + // For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners + string effectiveDir = directory; +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode); + if (isCursor && !string.IsNullOrEmpty(directory)) + { + // Replace canonical path segment with the symlink path if present + const string canonical = "/Library/Application Support/"; + const string symlinkSeg = "/Library/AppSupport/"; + try + { + // Normalize to full path style + if (directory.Contains(canonical)) + { + var candidate = directory.Replace(canonical, symlinkSeg).Replace('\\', '/'); + if (System.IO.Directory.Exists(candidate)) + { + effectiveDir = candidate; + } + } + else + { + // If installer returned XDG-style on macOS, map to canonical symlink + string norm = directory.Replace('\\', '/'); + int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal); + if (idx >= 0) + { + string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty; + string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/... + string candidate = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/'); + if (System.IO.Directory.Exists(candidate)) + { + effectiveDir = candidate; + } + } + } + } + catch { /* fallback to original directory on any error */ } + } +#endif + + unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" }); + + if (isVSCode) + { + unity["type"] = "stdio"; + } + else + { + // Remove type if it somehow exists from previous clients + if (unity["type"] != null) unity.Remove("type"); + } + + if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro)) + { + if (unity["env"] == null) + { + unity["env"] = new JObject(); + } + + if (unity["disabled"] == null) + { + unity["disabled"] = false; + } + } + } + + private static JObject EnsureObject(JObject parent, string name) + { + if (parent[name] is JObject o) return o; + var created = new JObject(); + parent[name] = created; + return created; + } + } +} diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs.meta b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs.meta new file mode 100644 index 00000000..f574fde7 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c07c3369f73943919d9e086a81d1dcc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/ExecPath.cs b/MCPForUnity/Editor/Helpers/ExecPath.cs new file mode 100644 index 00000000..20c1200b --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ExecPath.cs @@ -0,0 +1,278 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Runtime.InteropServices; +using UnityEditor; + +namespace MCPForUnity.Editor.Helpers +{ + internal static class ExecPath + { + private const string PrefClaude = "MCPForUnity.ClaudeCliPath"; + + // Resolve Claude CLI absolute path. Pref → env → common locations → PATH. + internal static string ResolveClaude() + { + try + { + string pref = EditorPrefs.GetString(PrefClaude, string.Empty); + if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref; + } + catch { } + + string env = Environment.GetEnvironmentVariable("CLAUDE_CLI"); + if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + string[] candidates = + { + "/opt/homebrew/bin/claude", + "/usr/local/bin/claude", + Path.Combine(home, ".local", "bin", "claude"), + }; + foreach (string c in candidates) { if (File.Exists(c)) return c; } + // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude + string nvmClaude = ResolveClaudeFromNvm(home); + if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude; +#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"); +#else + return null; +#endif + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { +#if UNITY_EDITOR_WIN + // Common npm global locations + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; + string[] candidates = + { + // Prefer .cmd (most reliable from non-interactive processes) + Path.Combine(appData, "npm", "claude.cmd"), + Path.Combine(localAppData, "npm", "claude.cmd"), + // Fall back to PowerShell shim if only .ps1 is present + Path.Combine(appData, "npm", "claude.ps1"), + Path.Combine(localAppData, "npm", "claude.ps1"), + }; + foreach (string c in candidates) { if (File.Exists(c)) return c; } + string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude"); + if (!string.IsNullOrEmpty(fromWhere)) return fromWhere; +#endif + return null; + } + + // Linux + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + string[] candidates = + { + "/usr/local/bin/claude", + "/usr/bin/claude", + Path.Combine(home, ".local", "bin", "claude"), + }; + foreach (string c in candidates) { if (File.Exists(c)) return c; } + // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude + string nvmClaude = ResolveClaudeFromNvm(home); + if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude; +#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + return Which("claude", "/usr/local/bin:/usr/bin:/bin"); +#else + return null; +#endif + } + } + + // Attempt to resolve claude from NVM-managed Node installations, choosing the newest version + private static string ResolveClaudeFromNvm(string home) + { + try + { + if (string.IsNullOrEmpty(home)) return null; + string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node"); + if (!Directory.Exists(nvmNodeDir)) return null; + + string bestPath = null; + Version bestVersion = null; + foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir)) + { + string name = Path.GetFileName(versionDir); + if (string.IsNullOrEmpty(name)) continue; + if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + // Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0 + string versionStr = name.Substring(1); + int dashIndex = versionStr.IndexOf('-'); + if (dashIndex > 0) + { + versionStr = versionStr.Substring(0, dashIndex); + } + if (Version.TryParse(versionStr, out Version parsed)) + { + string candidate = Path.Combine(versionDir, "bin", "claude"); + if (File.Exists(candidate)) + { + if (bestVersion == null || parsed > bestVersion) + { + bestVersion = parsed; + bestPath = candidate; + } + } + } + } + } + return bestPath; + } + catch { return null; } + } + + // Explicitly set the Claude CLI absolute path override in EditorPrefs + internal static void SetClaudeCliPath(string absolutePath) + { + try + { + if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath)) + { + EditorPrefs.SetString(PrefClaude, absolutePath); + } + } + catch { } + } + + // Clear any previously set Claude CLI override path + internal static void ClearClaudeCliPath() + { + try + { + if (EditorPrefs.HasKey(PrefClaude)) + { + EditorPrefs.DeleteKey(PrefClaude); + } + } + catch { } + } + + // Use existing UV resolver; returns absolute path or null. + internal static string ResolveUv() + { + return ServerInstaller.FindUvPath(); + } + + internal static bool TryRun( + string file, + string args, + string workingDir, + out string stdout, + out string stderr, + int timeoutMs = 15000, + string extraPathPrepend = null) + { + stdout = string.Empty; + stderr = string.Empty; + try + { + // Handle PowerShell scripts on Windows by invoking through powershell.exe + bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase); + + var psi = new ProcessStartInfo + { + FileName = isPs1 ? "powershell.exe" : file, + Arguments = isPs1 + ? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim() + : args, + WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + if (!string.IsNullOrEmpty(extraPathPrepend)) + { + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) + ? extraPathPrepend + : (extraPathPrepend + System.IO.Path.PathSeparator + currentPath); + } + + using var process = new Process { StartInfo = psi, EnableRaisingEvents = false }; + + var so = new StringBuilder(); + var se = new StringBuilder(); + process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); }; + process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); }; + + if (!process.Start()) return false; + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + if (!process.WaitForExit(timeoutMs)) + { + try { process.Kill(); } catch { } + return false; + } + + // Ensure async buffers are flushed + process.WaitForExit(); + + stdout = so.ToString(); + stderr = se.ToString(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + +#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + private static string Which(string exe, string prependPath) + { + try + { + var psi = new ProcessStartInfo("/usr/bin/which", exe) + { + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true, + }; + string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path); + using var p = Process.Start(psi); + string output = p?.StandardOutput.ReadToEnd().Trim(); + p?.WaitForExit(1500); + return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null; + } + catch { return null; } + } +#endif + +#if UNITY_EDITOR_WIN + private static string Where(string exe) + { + try + { + var psi = new ProcessStartInfo("where", exe) + { + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true, + }; + using var p = Process.Start(psi); + string first = p?.StandardOutput.ReadToEnd() + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .FirstOrDefault(); + p?.WaitForExit(1500); + return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null; + } + catch { return null; } + } +#endif + } +} diff --git a/MCPForUnity/Editor/Helpers/ExecPath.cs.meta b/MCPForUnity/Editor/Helpers/ExecPath.cs.meta new file mode 100644 index 00000000..aba921ed --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ExecPath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs b/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs new file mode 100644 index 00000000..d7bf979c --- /dev/null +++ b/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs @@ -0,0 +1,528 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Runtime.Serialization; // For Converters + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Handles serialization of GameObjects and Components for MCP responses. + /// Includes reflection helpers and caching for performance. + /// + public static class GameObjectSerializer + { + // --- Data Serialization --- + + /// + /// Creates a serializable representation of a GameObject. + /// + public static object GetGameObjectData(GameObject go) + { + if (go == null) + return null; + return new + { + name = go.name, + instanceID = go.GetInstanceID(), + tag = go.tag, + layer = go.layer, + activeSelf = go.activeSelf, + activeInHierarchy = go.activeInHierarchy, + isStatic = go.isStatic, + scenePath = go.scene.path, // Identify which scene it belongs to + transform = new // Serialize transform components carefully to avoid JSON issues + { + // Serialize Vector3 components individually to prevent self-referencing loops. + // The default serializer can struggle with properties like Vector3.normalized. + position = new + { + x = go.transform.position.x, + y = go.transform.position.y, + z = go.transform.position.z, + }, + localPosition = new + { + x = go.transform.localPosition.x, + y = go.transform.localPosition.y, + z = go.transform.localPosition.z, + }, + rotation = new + { + x = go.transform.rotation.eulerAngles.x, + y = go.transform.rotation.eulerAngles.y, + z = go.transform.rotation.eulerAngles.z, + }, + localRotation = new + { + x = go.transform.localRotation.eulerAngles.x, + y = go.transform.localRotation.eulerAngles.y, + z = go.transform.localRotation.eulerAngles.z, + }, + scale = new + { + x = go.transform.localScale.x, + y = go.transform.localScale.y, + z = go.transform.localScale.z, + }, + forward = new + { + x = go.transform.forward.x, + y = go.transform.forward.y, + z = go.transform.forward.z, + }, + up = new + { + x = go.transform.up.x, + y = go.transform.up.y, + z = go.transform.up.z, + }, + right = new + { + x = go.transform.right.x, + y = go.transform.right.y, + z = go.transform.right.z, + }, + }, + parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent + // Optionally include components, but can be large + // components = go.GetComponents().Select(c => GetComponentData(c)).ToList() + // Or just component names: + componentNames = go.GetComponents() + .Select(c => c.GetType().FullName) + .ToList(), + }; + } + + // --- Metadata Caching for Reflection --- + private class CachedMetadata + { + public readonly List SerializableProperties; + public readonly List SerializableFields; + + public CachedMetadata(List properties, List fields) + { + SerializableProperties = properties; + SerializableFields = fields; + } + } + // Key becomes Tuple + private static readonly Dictionary, CachedMetadata> _metadataCache = new Dictionary, CachedMetadata>(); + // --- End Metadata Caching --- + + /// + /// Creates a serializable representation of a Component, attempting to serialize + /// public properties and fields using reflection, with caching and control over non-public fields. + /// + // Add the flag parameter here + public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true) + { + // --- Add Early Logging --- + // Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})"); + // --- End Early Logging --- + + if (c == null) return null; + Type componentType = c.GetType(); + + // --- Special handling for Transform to avoid reflection crashes and problematic properties --- + if (componentType == typeof(Transform)) + { + Transform tr = c as Transform; + // Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})"); + return new Dictionary + { + { "typeName", componentType.FullName }, + { "instanceID", tr.GetInstanceID() }, + // Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'. + { "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject() ?? new JObject() }, // Use Euler angles + { "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject() ?? new JObject() }, + { "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 }, + { "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 }, + { "childCount", tr.childCount }, + // Include standard Object/Component properties + { "name", tr.name }, + { "tag", tr.tag }, + { "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 } + }; + } + // --- End Special handling for Transform --- + + // --- Special handling for Camera to avoid matrix-related crashes --- + if (componentType == typeof(Camera)) + { + Camera cam = c as Camera; + var cameraProperties = new Dictionary(); + + // List of safe properties to serialize + var safeProperties = new Dictionary> + { + { "nearClipPlane", () => cam.nearClipPlane }, + { "farClipPlane", () => cam.farClipPlane }, + { "fieldOfView", () => cam.fieldOfView }, + { "renderingPath", () => (int)cam.renderingPath }, + { "actualRenderingPath", () => (int)cam.actualRenderingPath }, + { "allowHDR", () => cam.allowHDR }, + { "allowMSAA", () => cam.allowMSAA }, + { "allowDynamicResolution", () => cam.allowDynamicResolution }, + { "forceIntoRenderTexture", () => cam.forceIntoRenderTexture }, + { "orthographicSize", () => cam.orthographicSize }, + { "orthographic", () => cam.orthographic }, + { "opaqueSortMode", () => (int)cam.opaqueSortMode }, + { "transparencySortMode", () => (int)cam.transparencySortMode }, + { "depth", () => cam.depth }, + { "aspect", () => cam.aspect }, + { "cullingMask", () => cam.cullingMask }, + { "eventMask", () => cam.eventMask }, + { "backgroundColor", () => cam.backgroundColor }, + { "clearFlags", () => (int)cam.clearFlags }, + { "stereoEnabled", () => cam.stereoEnabled }, + { "stereoSeparation", () => cam.stereoSeparation }, + { "stereoConvergence", () => cam.stereoConvergence }, + { "enabled", () => cam.enabled }, + { "name", () => cam.name }, + { "tag", () => cam.tag }, + { "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } } + }; + + foreach (var prop in safeProperties) + { + try + { + var value = prop.Value(); + if (value != null) + { + AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value); + } + } + catch (Exception) + { + // Silently skip any property that fails + continue; + } + } + + return new Dictionary + { + { "typeName", componentType.FullName }, + { "instanceID", cam.GetInstanceID() }, + { "properties", cameraProperties } + }; + } + // --- End Special handling for Camera --- + + var data = new Dictionary + { + { "typeName", componentType.FullName }, + { "instanceID", c.GetInstanceID() } + }; + + // --- Get Cached or Generate Metadata (using new cache key) --- + Tuple cacheKey = new Tuple(componentType, includeNonPublicSerializedFields); + if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData)) + { + var propertiesToCache = new List(); + var fieldsToCache = new List(); + + // Traverse the hierarchy from the component type up to MonoBehaviour + Type currentType = componentType; + while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object)) + { + // Get properties declared only at the current type level + BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + foreach (var propInfo in currentType.GetProperties(propFlags)) + { + // Basic filtering (readable, not indexer, not transform which is handled elsewhere) + if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; + // Add if not already added (handles overrides - keep the most derived version) + if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) + { + propertiesToCache.Add(propInfo); + } + } + + // Get fields declared only at the current type level (both public and non-public) + BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; + var declaredFields = currentType.GetFields(fieldFlags); + + // Process the declared Fields for caching + foreach (var fieldInfo in declaredFields) + { + if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields + + // Add if not already added (handles hiding - keep the most derived version) + if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; + + bool shouldInclude = false; + if (includeNonPublicSerializedFields) + { + // If TRUE, include Public OR NonPublic with [SerializeField] + shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false)); + } + else // includeNonPublicSerializedFields is FALSE + { + // If FALSE, include ONLY if it is explicitly Public. + shouldInclude = fieldInfo.IsPublic; + } + + if (shouldInclude) + { + fieldsToCache.Add(fieldInfo); + } + } + + // Move to the base type + currentType = currentType.BaseType; + } + // --- End Hierarchy Traversal --- + + cachedData = new CachedMetadata(propertiesToCache, fieldsToCache); + _metadataCache[cacheKey] = cachedData; // Add to cache with combined key + } + // --- End Get Cached or Generate Metadata --- + + // --- Use cached metadata --- + var serializablePropertiesOutput = new Dictionary(); + + // --- Add Logging Before Property Loop --- + // Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}..."); + // --- End Logging Before Property Loop --- + + // Use cached properties + foreach (var propInfo in cachedData.SerializableProperties) + { + string propName = propInfo.Name; + + // --- Skip known obsolete/problematic Component shortcut properties --- + bool skipProperty = false; + if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" || + propName == "light" || propName == "animation" || propName == "constantForce" || + propName == "renderer" || propName == "audio" || propName == "networkView" || + propName == "collider" || propName == "collider2D" || propName == "hingeJoint" || + propName == "particleSystem" || + // Also skip potentially problematic Matrix properties prone to cycles/errors + propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") + { + // Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log + skipProperty = true; + } + // --- End Skip Generic Properties --- + + // --- Skip specific potentially problematic Camera properties --- + if (componentType == typeof(Camera) && + (propName == "pixelRect" || + propName == "rect" || + propName == "cullingMatrix" || + propName == "useOcclusionCulling" || + propName == "worldToCameraMatrix" || + propName == "projectionMatrix" || + propName == "nonJitteredProjectionMatrix" || + propName == "previousViewProjectionMatrix" || + propName == "cameraToWorldMatrix")) + { + // Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}"); + skipProperty = true; + } + // --- End Skip Camera Properties --- + + // --- Skip specific potentially problematic Transform properties --- + if (componentType == typeof(Transform) && + (propName == "lossyScale" || + propName == "rotation" || + propName == "worldToLocalMatrix" || + propName == "localToWorldMatrix")) + { + // Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}"); + skipProperty = true; + } + // --- End Skip Transform Properties --- + + // Skip if flagged + if (skipProperty) + { + continue; + } + + try + { + // --- Add detailed logging --- + // Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}"); + // --- End detailed logging --- + object value = propInfo.GetValue(c); + Type propType = propInfo.PropertyType; + AddSerializableValue(serializablePropertiesOutput, propName, propType, value); + } + catch (Exception) + { + // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}"); + } + } + + // --- Add Logging Before Field Loop --- + // Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}..."); + // --- End Logging Before Field Loop --- + + // Use cached fields + foreach (var fieldInfo in cachedData.SerializableFields) + { + try + { + // --- Add detailed logging for fields --- + // Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}"); + // --- End detailed logging for fields --- + object value = fieldInfo.GetValue(c); + string fieldName = fieldInfo.Name; + Type fieldType = fieldInfo.FieldType; + AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value); + } + catch (Exception) + { + // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}"); + } + } + // --- End Use cached metadata --- + + if (serializablePropertiesOutput.Count > 0) + { + data["properties"] = serializablePropertiesOutput; + } + + return data; + } + + // Helper function to decide how to serialize different types + private static void AddSerializableValue(Dictionary dict, string name, Type type, object value) + { + // Simplified: Directly use CreateTokenFromValue which uses the serializer + if (value == null) + { + dict[name] = null; + return; + } + + try + { + // Use the helper that employs our custom serializer settings + JToken token = CreateTokenFromValue(value, type); + if (token != null) // Check if serialization succeeded in the helper + { + // Convert JToken back to a basic object structure for the dictionary + dict[name] = ConvertJTokenToPlainObject(token); + } + // If token is null, it means serialization failed and a warning was logged. + } + catch (Exception e) + { + // Catch potential errors during JToken conversion or addition to dictionary + Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); + } + } + + // Helper to convert JToken back to basic object structure + private static object ConvertJTokenToPlainObject(JToken token) + { + if (token == null) return null; + + switch (token.Type) + { + case JTokenType.Object: + var objDict = new Dictionary(); + foreach (var prop in ((JObject)token).Properties()) + { + objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value); + } + return objDict; + + case JTokenType.Array: + var list = new List(); + foreach (var item in (JArray)token) + { + list.Add(ConvertJTokenToPlainObject(item)); + } + return list; + + case JTokenType.Integer: + return token.ToObject(); // Use long for safety + case JTokenType.Float: + return token.ToObject(); // Use double for safety + case JTokenType.String: + return token.ToObject(); + case JTokenType.Boolean: + return token.ToObject(); + case JTokenType.Date: + return token.ToObject(); + case JTokenType.Guid: + return token.ToObject(); + case JTokenType.Uri: + return token.ToObject(); + case JTokenType.TimeSpan: + return token.ToObject(); + case JTokenType.Bytes: + return token.ToObject(); + case JTokenType.Null: + return null; + case JTokenType.Undefined: + return null; // Treat undefined as null + + default: + // Fallback for simple value types not explicitly listed + if (token is JValue jValue && jValue.Value != null) + { + return jValue.Value; + } + // Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null."); + return null; + } + } + + // --- Define custom JsonSerializerSettings for OUTPUT --- + private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings + { + Converters = new List + { + new Vector3Converter(), + new Vector2Converter(), + new QuaternionConverter(), + new ColorConverter(), + new RectConverter(), + new BoundsConverter(), + new UnityEngineObjectConverter() // Handles serialization of references + }, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed + }; + private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings); + // --- End Define custom JsonSerializerSettings --- + + // Helper to create JToken using the output serializer + private static JToken CreateTokenFromValue(object value, Type type) + { + if (value == null) return JValue.CreateNull(); + + try + { + // Use the pre-configured OUTPUT serializer instance + return JToken.FromObject(value, _outputSerializer); + } + catch (JsonSerializationException e) + { + Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); + return null; // Indicate serialization failure + } + catch (Exception e) // Catch other unexpected errors + { + Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); + return null; // Indicate serialization failure + } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs.meta b/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs.meta new file mode 100644 index 00000000..9eb69d04 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64b8ff807bc9a401c82015cbafccffac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs new file mode 100644 index 00000000..389d47d2 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs @@ -0,0 +1,186 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using UnityEditor; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Shared helpers for reading and writing MCP client configuration files. + /// Consolidates file atomics and server directory resolution so the editor + /// window can focus on UI concerns only. + /// + public static class McpConfigFileHelper + { + public static string ExtractDirectoryArg(string[] args) + { + if (args == null) return null; + for (int i = 0; i < args.Length - 1; i++) + { + if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } + + public static bool PathsEqual(string a, string b) + { + if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false; + try + { + string na = Path.GetFullPath(a.Trim()); + string nb = Path.GetFullPath(b.Trim()); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase); + } + return string.Equals(na, nb, StringComparison.Ordinal); + } + catch + { + return false; + } + } + + /// + /// Resolves the server directory to use for MCP tools, preferring + /// existing config values and falling back to installed/embedded copies. + /// + public static string ResolveServerDirectory(string pythonDir, string[] existingArgs) + { + string serverSrc = ExtractDirectoryArg(existingArgs); + bool serverValid = !string.IsNullOrEmpty(serverSrc) + && File.Exists(Path.Combine(serverSrc, "server.py")); + if (!serverValid) + { + if (!string.IsNullOrEmpty(pythonDir) + && File.Exists(Path.Combine(pythonDir, "server.py"))) + { + serverSrc = pythonDir; + } + else + { + serverSrc = ResolveServerSource(); + } + } + + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc)) + { + string norm = serverSrc.Replace('\\', '/'); + int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal); + if (idx >= 0) + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; + string suffix = norm.Substring(idx + "/.local/share/".Length); + serverSrc = Path.Combine(home, "Library", "Application Support", suffix); + } + } + } + catch + { + // Ignore failures and fall back to the original path. + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + && !string.IsNullOrEmpty(serverSrc) + && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0 + && !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false)) + { + serverSrc = ServerInstaller.GetServerPath(); + } + + return serverSrc; + } + + public static void WriteAtomicFile(string path, string contents) + { + string tmp = path + ".tmp"; + string backup = path + ".backup"; + bool writeDone = false; + try + { + File.WriteAllText(tmp, contents, new UTF8Encoding(false)); + try + { + File.Replace(tmp, path, backup); + writeDone = true; + } + catch (FileNotFoundException) + { + File.Move(tmp, path); + writeDone = true; + } + catch (PlatformNotSupportedException) + { + if (File.Exists(path)) + { + try + { + if (File.Exists(backup)) File.Delete(backup); + } + catch { } + File.Move(path, backup); + } + File.Move(tmp, path); + writeDone = true; + } + } + catch (Exception ex) + { + try + { + if (!writeDone && File.Exists(backup)) + { + try { File.Copy(backup, path, true); } catch { } + } + } + catch { } + throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex); + } + finally + { + try { if (File.Exists(tmp)) File.Delete(tmp); } catch { } + try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { } + } + } + + public static string ResolveServerSource() + { + try + { + string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty); + if (!string.IsNullOrEmpty(remembered) + && File.Exists(Path.Combine(remembered, "server.py"))) + { + return remembered; + } + + ServerInstaller.EnsureServerInstalled(); + string installed = ServerInstaller.GetServerPath(); + if (File.Exists(Path.Combine(installed, "server.py"))) + { + return installed; + } + + bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false); + if (useEmbedded + && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded) + && File.Exists(Path.Combine(embedded, "server.py"))) + { + return embedded; + } + + return installed; + } + catch + { + return ServerInstaller.GetServerPath(); + } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta b/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta new file mode 100644 index 00000000..8f81ae99 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f69ad468942b74c0ea24e3e8e5f21a4b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs new file mode 100644 index 00000000..8e727efb --- /dev/null +++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs @@ -0,0 +1,297 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Shared helper for MCP client configuration management with sophisticated + /// logic for preserving existing configs and handling different client types + /// + public static class McpConfigurationHelper + { + private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig"; + + /// + /// Writes MCP configuration to the specified path using sophisticated logic + /// that preserves existing configuration and only writes when necessary + /// + public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null) + { + // 0) Respect explicit lock (hidden pref or UI toggle) + try + { + if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false)) + return "Skipped (locked)"; + } + catch { } + + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + + // Read existing config if it exists + string existingJson = "{}"; + if (File.Exists(configPath)) + { + try + { + existingJson = File.ReadAllText(configPath); + } + catch (Exception e) + { + Debug.LogWarning($"Error reading existing config: {e.Message}."); + } + } + + // Parse the existing JSON while preserving all properties + dynamic existingConfig; + try + { + if (string.IsNullOrWhiteSpace(existingJson)) + { + existingConfig = new JObject(); + } + else + { + existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject(); + } + } + catch + { + // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object + if (!string.IsNullOrWhiteSpace(existingJson)) + { + Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block."); + } + existingConfig = new JObject(); + } + + // Determine existing entry references (command/args) + string existingCommand = null; + string[] existingArgs = null; + bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode); + try + { + if (isVSCode) + { + existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); + existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject(); + } + else + { + existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); + existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject(); + } + } + catch { } + + // 1) Start from existing, only fill gaps (prefer trusted resolver) + string uvPath = ServerInstaller.FindUvPath(); + // Optionally trust existingCommand if it looks like uv/uv.exe + try + { + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + { + uvPath = existingCommand; + } + } + catch { } + if (uvPath == null) return "UV package manager not found. Please install UV first."; + string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + + // 2) Canonical args order + var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; + + // 3) Only write if changed + bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) + || !ArgsEqual(existingArgs, newArgs); + if (!changed) + { + return "Configured successfully"; // nothing to do + } + + // 4) Ensure containers exist and write back minimal changes + JObject existingRoot; + if (existingConfig is JObject eo) + existingRoot = eo; + else + existingRoot = JObject.FromObject(existingConfig); + + existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient); + + string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); + + McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); + + try + { + if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); + EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); + } + catch { } + + return "Configured successfully"; + } + + /// + /// Configures a Codex client with sophisticated TOML handling + /// + public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient) + { + try + { + if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false)) + return "Skipped (locked)"; + } + catch { } + + string existingToml = string.Empty; + if (File.Exists(configPath)) + { + try + { + existingToml = File.ReadAllText(configPath); + } + catch (Exception e) + { + Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}"); + existingToml = string.Empty; + } + } + + string existingCommand = null; + string[] existingArgs = null; + if (!string.IsNullOrWhiteSpace(existingToml)) + { + CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); + } + + string uvPath = ServerInstaller.FindUvPath(); + try + { + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + { + uvPath = existingCommand; + } + } + catch { } + + if (uvPath == null) + { + return "UV package manager not found. Please install UV first."; + } + + string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; + + bool changed = true; + if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null) + { + changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) + || !ArgsEqual(existingArgs, newArgs); + } + + if (!changed) + { + return "Configured successfully"; + } + + string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc); + string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock); + + McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml); + + try + { + if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); + EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); + } + catch { } + + return "Configured successfully"; + } + + /// + /// Validates UV binary by running --version command + /// + private static bool IsValidUvBinary(string path) + { + try + { + if (!File.Exists(path)) return false; + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = path, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = System.Diagnostics.Process.Start(psi); + if (p == null) return false; + if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; } + if (p.ExitCode != 0) return false; + string output = p.StandardOutput.ReadToEnd().Trim(); + return output.StartsWith("uv "); + } + catch { return false; } + } + + /// + /// Compares two string arrays for equality + /// + private static bool ArgsEqual(string[] a, string[] b) + { + if (a == null || b == null) return a == b; + if (a.Length != b.Length) return false; + for (int i = 0; i < a.Length; i++) + { + if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false; + } + return true; + } + + /// + /// Gets the appropriate config file path for the given MCP client based on OS + /// + public static string GetClientConfigPath(McpClient mcpClient) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return mcpClient.windowsConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return string.IsNullOrEmpty(mcpClient.macConfigPath) + ? mcpClient.linuxConfigPath + : mcpClient.macConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return mcpClient.linuxConfigPath; + } + else + { + return mcpClient.linuxConfigPath; // fallback + } + } + + /// + /// Creates the directory for the config file if it doesn't exist + /// + public static void EnsureConfigDirectoryExists(string configPath) + { + Directory.CreateDirectory(Path.GetDirectoryName(configPath)); + } + } +} diff --git a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs.meta b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs.meta new file mode 100644 index 00000000..17de56c8 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e45ac2a13b4c1ba468b8e3aa67b292ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/McpLog.cs b/MCPForUnity/Editor/Helpers/McpLog.cs new file mode 100644 index 00000000..85abdb79 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/McpLog.cs @@ -0,0 +1,31 @@ +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + internal static class McpLog + { + private const string Prefix = "MCP-FOR-UNITY:"; + + private static bool IsDebugEnabled() + { + try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); } catch { return false; } + } + + public static void Info(string message, bool always = true) + { + if (!always && !IsDebugEnabled()) return; + Debug.Log($"{Prefix} {message}"); + } + + public static void Warn(string message) + { + Debug.LogWarning($"{Prefix} {message}"); + } + + public static void Error(string message) + { + Debug.LogError($"{Prefix} {message}"); + } + } +} diff --git a/MCPForUnity/Editor/Helpers/McpLog.cs.meta b/MCPForUnity/Editor/Helpers/McpLog.cs.meta new file mode 100644 index 00000000..b9e0fc38 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/McpLog.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + + diff --git a/MCPForUnity/Editor/Helpers/McpPathResolver.cs b/MCPForUnity/Editor/Helpers/McpPathResolver.cs new file mode 100644 index 00000000..8e683965 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/McpPathResolver.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using UnityEngine; +using UnityEditor; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Shared helper for resolving Python server directory paths with support for + /// development mode, embedded servers, and installed packages + /// + public static class McpPathResolver + { + private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer"; + + /// + /// Resolves the Python server directory path with comprehensive logic + /// including development mode support and fallback mechanisms + /// + public static string FindPackagePythonDirectory(bool debugLogsEnabled = false) + { + string pythonDir = McpConfigFileHelper.ResolveServerSource(); + + try + { + // Only check dev paths if we're using a file-based package (development mode) + bool isDevelopmentMode = IsDevelopmentMode(); + if (isDevelopmentMode) + { + string currentPackagePath = Path.GetDirectoryName(Application.dataPath); + string[] devPaths = { + Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"), + Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"), + }; + + foreach (string devPath in devPaths) + { + if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py"))) + { + if (debugLogsEnabled) + { + Debug.Log($"Currently in development mode. Package: {devPath}"); + } + return devPath; + } + } + } + + // Resolve via shared helper (handles local registry and older fallback) only if dev override on + if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false)) + { + if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) + { + return embedded; + } + } + + // Log only if the resolved path does not actually contain server.py + if (debugLogsEnabled) + { + bool hasServer = false; + try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { } + if (!hasServer) + { + Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path"); + } + } + } + catch (Exception e) + { + Debug.LogError($"Error finding package path: {e.Message}"); + } + + return pythonDir; + } + + /// + /// Checks if the current Unity project is in development mode + /// (i.e., the package is referenced as a local file path in manifest.json) + /// + private static bool IsDevelopmentMode() + { + try + { + // Only treat as development if manifest explicitly references a local file path for the package + string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json"); + if (!File.Exists(manifestPath)) return false; + + string manifestContent = File.ReadAllText(manifestPath); + // Look specifically for our package dependency set to a file: URL + // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk + if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0) + { + int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase); + // Crude but effective: check for "file:" in the same line/value + if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0 + && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + catch + { + return false; + } + } + + /// + /// Gets the appropriate PATH prepend for the current platform when running external processes + /// + public static string GetPathPrepend() + { + if (Application.platform == RuntimePlatform.OSXEditor) + return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; + else if (Application.platform == RuntimePlatform.LinuxEditor) + return "/usr/local/bin:/usr/bin:/bin"; + return null; + } + } +} diff --git a/MCPForUnity/Editor/Helpers/McpPathResolver.cs.meta b/MCPForUnity/Editor/Helpers/McpPathResolver.cs.meta new file mode 100644 index 00000000..38f19973 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/McpPathResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c76f0c7ff138ba4a952481e04bc3974 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs b/MCPForUnity/Editor/Helpers/PackageDetector.cs new file mode 100644 index 00000000..bb8861fe --- /dev/null +++ b/MCPForUnity/Editor/Helpers/PackageDetector.cs @@ -0,0 +1,107 @@ +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Auto-runs legacy/older install detection on package load/update (log-only). + /// Runs once per embedded server version using an EditorPrefs version-scoped key. + /// + [InitializeOnLoad] + public static class PackageDetector + { + private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:"; + + static PackageDetector() + { + try + { + string pkgVer = ReadPackageVersionOrFallback(); + string key = DetectOnceFlagKeyPrefix + pkgVer; + + // Always force-run if legacy roots exist or canonical install is missing + bool legacyPresent = LegacyRootsExist(); + bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py")); + + if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing) + { + // Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs. + EditorApplication.delayCall += () => + { + string error = null; + System.Exception capturedEx = null; + try + { + // Ensure any UnityEditor API usage inside runs on the main thread + ServerInstaller.EnsureServerInstalled(); + } + catch (System.Exception ex) + { + error = ex.Message; + capturedEx = ex; + } + + // Unity APIs must stay on main thread + try { EditorPrefs.SetBool(key, true); } catch { } + // Ensure prefs cleanup happens on main thread + try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { } + try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { } + + if (!string.IsNullOrEmpty(error)) + { + Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}"); + // Alternatively: Debug.LogException(capturedEx); + } + }; + } + } + catch { /* ignore */ } + } + + private static string ReadEmbeddedVersionOrFallback() + { + try + { + if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc)) + { + var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt"); + if (System.IO.File.Exists(p)) + return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown"); + } + } + catch { } + return "unknown"; + } + + private static string ReadPackageVersionOrFallback() + { + try + { + var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly); + if (info != null && !string.IsNullOrEmpty(info.version)) return info.version; + } + catch { } + // Fallback to embedded server version if package info unavailable + return ReadEmbeddedVersionOrFallback(); + } + + private static bool LegacyRootsExist() + { + try + { + string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty; + string[] roots = + { + System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"), + System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src") + }; + foreach (var r in roots) + { + try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { } + } + } + catch { } + return false; + } + } +} diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs.meta b/MCPForUnity/Editor/Helpers/PackageDetector.cs.meta new file mode 100644 index 00000000..f1a5dbe4 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/PackageDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b82eaef548d164ca095f17db64d15af8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/PackageInstaller.cs b/MCPForUnity/Editor/Helpers/PackageInstaller.cs new file mode 100644 index 00000000..031a6aed --- /dev/null +++ b/MCPForUnity/Editor/Helpers/PackageInstaller.cs @@ -0,0 +1,43 @@ +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Handles automatic installation of the Python server when the package is first installed. + /// + [InitializeOnLoad] + public static class PackageInstaller + { + private const string InstallationFlagKey = "MCPForUnity.ServerInstalled"; + + static PackageInstaller() + { + // Check if this is the first time the package is loaded + if (!EditorPrefs.GetBool(InstallationFlagKey, false)) + { + // Schedule the installation for after Unity is fully loaded + EditorApplication.delayCall += InstallServerOnFirstLoad; + } + } + + private static void InstallServerOnFirstLoad() + { + try + { + Debug.Log("MCP-FOR-UNITY: Installing Python server..."); + ServerInstaller.EnsureServerInstalled(); + + // Mark as installed + EditorPrefs.SetBool(InstallationFlagKey, true); + + Debug.Log("MCP-FOR-UNITY: Python server installation completed successfully."); + } + catch (System.Exception ex) + { + Debug.LogError($"MCP-FOR-UNITY: Failed to install Python server: {ex.Message}"); + Debug.LogWarning("MCP-FOR-UNITY: You may need to manually install the Python server. Check the MCP For Unity Window for instructions."); + } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta b/MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta new file mode 100644 index 00000000..156e75fb --- /dev/null +++ b/MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19e6eaa637484e9fa19f9a0459809de2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/PortManager.cs b/MCPForUnity/Editor/Helpers/PortManager.cs new file mode 100644 index 00000000..09d85798 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/PortManager.cs @@ -0,0 +1,319 @@ +using System; +using System.IO; +using UnityEditor; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using Newtonsoft.Json; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Manages dynamic port allocation and persistent storage for MCP for Unity + /// + public static class PortManager + { + private static bool IsDebugEnabled() + { + try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); } + catch { return false; } + } + + private const int DefaultPort = 6400; + private const int MaxPortAttempts = 100; + private const string RegistryFileName = "unity-mcp-port.json"; + + [Serializable] + public class PortConfig + { + public int unity_port; + public string created_date; + public string project_path; + } + + /// + /// Get the port to use - either from storage or discover a new one + /// Will try stored port first, then fallback to discovering new port + /// + /// Port number to use + public static int GetPortWithFallback() + { + // Try to load stored port first, but only if it's from the current project + var storedConfig = GetStoredPortConfig(); + if (storedConfig != null && + storedConfig.unity_port > 0 && + string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase) && + IsPortAvailable(storedConfig.unity_port)) + { + if (IsDebugEnabled()) Debug.Log($"MCP-FOR-UNITY: Using stored port {storedConfig.unity_port} for current project"); + return storedConfig.unity_port; + } + + // If stored port exists but is currently busy, wait briefly for release + if (storedConfig != null && storedConfig.unity_port > 0) + { + if (WaitForPortRelease(storedConfig.unity_port, 1500)) + { + if (IsDebugEnabled()) Debug.Log($"MCP-FOR-UNITY: Stored port {storedConfig.unity_port} became available after short wait"); + return storedConfig.unity_port; + } + // Prefer sticking to the same port; let the caller handle bind retries/fallbacks + return storedConfig.unity_port; + } + + // If no valid stored port, find a new one and save it + int newPort = FindAvailablePort(); + SavePort(newPort); + return newPort; + } + + /// + /// Discover and save a new available port (used by Auto-Connect button) + /// + /// New available port + public static int DiscoverNewPort() + { + int newPort = FindAvailablePort(); + SavePort(newPort); + if (IsDebugEnabled()) Debug.Log($"MCP-FOR-UNITY: Discovered and saved new port: {newPort}"); + return newPort; + } + + /// + /// Find an available port starting from the default port + /// + /// Available port number + private static int FindAvailablePort() + { + // Always try default port first + if (IsPortAvailable(DefaultPort)) + { + if (IsDebugEnabled()) Debug.Log($"MCP-FOR-UNITY: Using default port {DefaultPort}"); + return DefaultPort; + } + + if (IsDebugEnabled()) Debug.Log($"MCP-FOR-UNITY: Default port {DefaultPort} is in use, searching for alternative..."); + + // Search for alternatives + for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++) + { + if (IsPortAvailable(port)) + { + if (IsDebugEnabled()) Debug.Log($"MCP-FOR-UNITY: Found available port {port}"); + return port; + } + } + + throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}"); + } + + /// + /// Check if a specific port is available for binding + /// + /// Port to check + /// True if port is available + public static bool IsPortAvailable(int port) + { + try + { + var testListener = new TcpListener(IPAddress.Loopback, port); + testListener.Start(); + testListener.Stop(); + return true; + } + catch (SocketException) + { + return false; + } + } + + /// + /// Check if a port is currently being used by MCP for Unity + /// This helps avoid unnecessary port changes when Unity itself is using the port + /// + /// Port to check + /// True if port appears to be used by MCP for Unity + public static bool IsPortUsedByMCPForUnity(int port) + { + try + { + // Try to make a quick connection to see if it's an MCP for Unity server + using var client = new TcpClient(); + var connectTask = client.ConnectAsync(IPAddress.Loopback, port); + if (connectTask.Wait(100)) // 100ms timeout + { + // If connection succeeded, it's likely the MCP for Unity server + return client.Connected; + } + return false; + } + catch + { + return false; + } + } + + /// + /// Wait for a port to become available for a limited amount of time. + /// Used to bridge the gap during domain reload when the old listener + /// hasn't released the socket yet. + /// + private static bool WaitForPortRelease(int port, int timeoutMs) + { + int waited = 0; + const int step = 100; + while (waited < timeoutMs) + { + if (IsPortAvailable(port)) + { + return true; + } + + // If the port is in use by an MCP instance, continue waiting briefly + if (!IsPortUsedByMCPForUnity(port)) + { + // In use by something else; don't keep waiting + return false; + } + + Thread.Sleep(step); + waited += step; + } + return IsPortAvailable(port); + } + + /// + /// Save port to persistent storage + /// + /// Port to save + private static void SavePort(int port) + { + try + { + var portConfig = new PortConfig + { + unity_port = port, + created_date = DateTime.UtcNow.ToString("O"), + project_path = Application.dataPath + }; + + string registryDir = GetRegistryDirectory(); + Directory.CreateDirectory(registryDir); + + string registryFile = GetRegistryFilePath(); + string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented); + // Write to hashed, project-scoped file + File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false)); + // Also write to legacy stable filename to avoid hash/case drift across reloads + string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName); + File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false)); + + if (IsDebugEnabled()) Debug.Log($"MCP-FOR-UNITY: Saved port {port} to storage"); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not save port to storage: {ex.Message}"); + } + } + + /// + /// Load port from persistent storage + /// + /// Stored port number, or 0 if not found + private static int LoadStoredPort() + { + try + { + string registryFile = GetRegistryFilePath(); + + if (!File.Exists(registryFile)) + { + // Backwards compatibility: try the legacy file name + string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName); + if (!File.Exists(legacy)) + { + return 0; + } + registryFile = legacy; + } + + string json = File.ReadAllText(registryFile); + var portConfig = JsonConvert.DeserializeObject(json); + + return portConfig?.unity_port ?? 0; + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load port from storage: {ex.Message}"); + return 0; + } + } + + /// + /// Get the current stored port configuration + /// + /// Port configuration if exists, null otherwise + public static PortConfig GetStoredPortConfig() + { + try + { + string registryFile = GetRegistryFilePath(); + + if (!File.Exists(registryFile)) + { + // Backwards compatibility: try the legacy file + string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName); + if (!File.Exists(legacy)) + { + return null; + } + registryFile = legacy; + } + + string json = File.ReadAllText(registryFile); + return JsonConvert.DeserializeObject(json); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load port config: {ex.Message}"); + return null; + } + } + + private static string GetRegistryDirectory() + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp"); + } + + private static string GetRegistryFilePath() + { + string dir = GetRegistryDirectory(); + string hash = ComputeProjectHash(Application.dataPath); + string fileName = $"unity-mcp-port-{hash}.json"; + return Path.Combine(dir, fileName); + } + + private static string ComputeProjectHash(string input) + { + try + { + using SHA1 sha1 = SHA1.Create(); + byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty); + byte[] hashBytes = sha1.ComputeHash(bytes); + var sb = new StringBuilder(); + foreach (byte b in hashBytes) + { + sb.Append(b.ToString("x2")); + } + return sb.ToString()[..8]; // short, sufficient for filenames + } + catch + { + return "default"; + } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/PortManager.cs.meta b/MCPForUnity/Editor/Helpers/PortManager.cs.meta new file mode 100644 index 00000000..ee3f667c --- /dev/null +++ b/MCPForUnity/Editor/Helpers/PortManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1b2c3d4e5f6789012345678901234ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Helpers/Response.cs b/MCPForUnity/Editor/Helpers/Response.cs new file mode 100644 index 00000000..cfcd2efb --- /dev/null +++ b/MCPForUnity/Editor/Helpers/Response.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Provides static methods for creating standardized success and error response objects. + /// Ensures consistent JSON structure for communication back to the Python server. + /// + public static class Response + { + /// + /// Creates a standardized success response object. + /// + /// A message describing the successful operation. + /// Optional additional data to include in the response. + /// An object representing the success response. + public static object Success(string message, object data = null) + { + if (data != null) + { + return new + { + success = true, + message = message, + data = data, + }; + } + else + { + return new { success = true, message = message }; + } + } + + /// + /// Creates a standardized error response object. + /// + /// A message describing the error. + /// Optional additional data (e.g., error details) to include. + /// An object representing the error response. + public static object Error(string errorCodeOrMessage, object data = null) + { + if (data != null) + { + // Note: The key is "error" for error messages, not "message" + return new + { + success = false, + // Preserve original behavior while adding a machine-parsable code field. + // If callers pass a code string, it will be echoed in both code and error. + code = errorCodeOrMessage, + error = errorCodeOrMessage, + data = data, + }; + } + else + { + return new { success = false, code = errorCodeOrMessage, error = errorCodeOrMessage }; + } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/Response.cs.meta b/MCPForUnity/Editor/Helpers/Response.cs.meta new file mode 100644 index 00000000..6fd11e39 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/Response.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80c09a76b944f8c4691e06c4d76c4be8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/ServerInstaller.cs b/MCPForUnity/Editor/Helpers/ServerInstaller.cs new file mode 100644 index 00000000..f41e03c3 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs @@ -0,0 +1,700 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + public static class ServerInstaller + { + private const string RootFolder = "UnityMCP"; + private const string ServerFolder = "UnityMcpServer"; + private const string VersionFileName = "server_version.txt"; + + /// + /// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source. + /// No network calls or Git operations are performed. + /// + public static void EnsureServerInstalled() + { + try + { + string saveLocation = GetSaveLocation(); + TryCreateMacSymlinkForAppSupport(); + string destRoot = Path.Combine(saveLocation, ServerFolder); + string destSrc = Path.Combine(destRoot, "src"); + + // Detect legacy installs and version state (logs) + DetectAndLogLegacyInstallStates(destRoot); + + // Resolve embedded source and versions + if (!TryGetEmbeddedServerSource(out string embeddedSrc)) + { + throw new Exception("Could not find embedded UnityMcpServer/src in the package."); + } + string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown"; + string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName)); + + bool destHasServer = File.Exists(Path.Combine(destSrc, "server.py")); + bool needOverwrite = !destHasServer + || string.IsNullOrEmpty(installedVer) + || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0); + + // Ensure destination exists + Directory.CreateDirectory(destRoot); + + if (needOverwrite) + { + // Copy the entire UnityMcpServer folder (parent of src) + string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer + CopyDirectoryRecursive(embeddedRoot, destRoot); + // Write/refresh version file + try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { } + McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer})."); + } + + // Cleanup legacy installs that are missing version or older than embedded + foreach (var legacyRoot in GetLegacyRootsForDetection()) + { + try + { + string legacySrc = Path.Combine(legacyRoot, "src"); + if (!File.Exists(Path.Combine(legacySrc, "server.py"))) continue; + string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName)); + bool legacyOlder = string.IsNullOrEmpty(legacyVer) + || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0); + if (legacyOlder) + { + TryKillUvForPath(legacySrc); + try + { + Directory.Delete(legacyRoot, recursive: true); + McpLog.Info($"Removed legacy server at '{legacyRoot}'."); + } + catch (Exception ex) + { + McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}"); + } + } + } + catch { } + } + + // Clear overrides that might point at legacy locations + try + { + EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); + EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); + } + catch { } + return; + } + catch (Exception ex) + { + // If a usable server is already present (installed or embedded), don't fail hard—just warn. + bool hasInstalled = false; + try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { } + + if (hasInstalled || TryGetEmbeddedServerSource(out _)) + { + McpLog.Warn($"Using existing server; skipped install. Details: {ex.Message}"); + return; + } + + McpLog.Error($"Failed to ensure server installation: {ex.Message}"); + } + } + + public static string GetServerPath() + { + return Path.Combine(GetSaveLocation(), ServerFolder, "src"); + } + + /// + /// Gets the platform-specific save location for the server. + /// + private static string GetSaveLocation() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Use per-user LocalApplicationData for canonical install location + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local"); + return Path.Combine(localAppData, RootFolder); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + if (string.IsNullOrEmpty(xdg)) + { + xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, + ".local", "share"); + } + return Path.Combine(xdg, RootFolder); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // On macOS, use LocalApplicationData (~/Library/Application Support) + var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + // Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support + bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share"); + if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg) + { + // Fallback: construct from $HOME + var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; + localAppSupport = Path.Combine(home, "Library", "Application Support"); + } + TryCreateMacSymlinkForAppSupport(); + return Path.Combine(localAppSupport, RootFolder); + } + throw new Exception("Unsupported operating system."); + } + + /// + /// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support + /// to mitigate arg parsing and quoting issues in some MCP clients. + /// Safe to call repeatedly. + /// + private static void TryCreateMacSymlinkForAppSupport() + { + try + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return; + string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; + if (string.IsNullOrEmpty(home)) return; + + string canonical = Path.Combine(home, "Library", "Application Support"); + string symlink = Path.Combine(home, "Library", "AppSupport"); + + // If symlink exists already, nothing to do + if (Directory.Exists(symlink) || File.Exists(symlink)) return; + + // Create symlink only if canonical exists + if (!Directory.Exists(canonical)) return; + + // Use 'ln -s' to create a directory symlink (macOS) + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = "/bin/ln", + Arguments = $"-s \"{canonical}\" \"{symlink}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = System.Diagnostics.Process.Start(psi); + p?.WaitForExit(2000); + } + catch { /* best-effort */ } + } + + private static bool IsDirectoryWritable(string path) + { + try + { + File.Create(Path.Combine(path, "test.txt")).Dispose(); + File.Delete(Path.Combine(path, "test.txt")); + return true; + } + catch + { + return false; + } + } + + /// + /// Checks if the server is installed at the specified location. + /// + private static bool IsServerInstalled(string location) + { + return Directory.Exists(location) + && File.Exists(Path.Combine(location, ServerFolder, "src", "server.py")); + } + + /// + /// Detects legacy installs or older versions and logs findings (no deletion yet). + /// + private static void DetectAndLogLegacyInstallStates(string canonicalRoot) + { + try + { + string canonicalSrc = Path.Combine(canonicalRoot, "src"); + // Normalize canonical root for comparisons + string normCanonicalRoot = NormalizePathSafe(canonicalRoot); + string embeddedSrc = null; + TryGetEmbeddedServerSource(out embeddedSrc); + + string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc ?? string.Empty, VersionFileName)); + string installedVer = ReadVersionFile(Path.Combine(canonicalSrc, VersionFileName)); + + // Legacy paths (macOS/Linux .config; Windows roaming as example) + foreach (var legacyRoot in GetLegacyRootsForDetection()) + { + // Skip logging for the canonical root itself + if (PathsEqualSafe(legacyRoot, normCanonicalRoot)) + continue; + string legacySrc = Path.Combine(legacyRoot, "src"); + bool hasServer = File.Exists(Path.Combine(legacySrc, "server.py")); + string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName)); + + if (hasServer) + { + // Case 1: No version file + if (string.IsNullOrEmpty(legacyVer)) + { + McpLog.Info("Detected legacy install without version file at: " + legacyRoot, always: false); + } + + // Case 2: Lives in legacy path + McpLog.Info("Detected legacy install path: " + legacyRoot, always: false); + + // Case 3: Has version but appears older than embedded + if (!string.IsNullOrEmpty(embeddedVer) && !string.IsNullOrEmpty(legacyVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0) + { + McpLog.Info($"Legacy install version {legacyVer} is older than embedded {embeddedVer}", always: false); + } + } + } + + // Also log if canonical is missing version (treated as older) + if (Directory.Exists(canonicalRoot)) + { + if (string.IsNullOrEmpty(installedVer)) + { + McpLog.Info("Canonical install missing version file (treat as older). Path: " + canonicalRoot, always: false); + } + else if (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0) + { + McpLog.Info($"Canonical install version {installedVer} is older than embedded {embeddedVer}", always: false); + } + } + } + catch (Exception ex) + { + McpLog.Warn("Detect legacy/version state failed: " + ex.Message); + } + } + + private static string NormalizePathSafe(string path) + { + try { return string.IsNullOrEmpty(path) ? path : Path.GetFullPath(path.Trim()); } + catch { return path; } + } + + private static bool PathsEqualSafe(string a, string b) + { + if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false; + string na = NormalizePathSafe(a); + string nb = NormalizePathSafe(b); + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase); + } + return string.Equals(na, nb, StringComparison.Ordinal); + } + catch { return false; } + } + + private static IEnumerable GetLegacyRootsForDetection() + { + var roots = new System.Collections.Generic.List(); + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + // macOS/Linux legacy + roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer")); + roots.Add(Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer")); + // Windows roaming example + try + { + string roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; + if (!string.IsNullOrEmpty(roaming)) + roots.Add(Path.Combine(roaming, "UnityMCP", "UnityMcpServer")); + // Windows legacy: early installers/dev scripts used %LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer + // Detect this location so we can clean up older copies during install/update. + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; + if (!string.IsNullOrEmpty(localAppData)) + roots.Add(Path.Combine(localAppData, "Programs", "UnityMCP", "UnityMcpServer")); + } + catch { } + return roots; + } + + private static void TryKillUvForPath(string serverSrcPath) + { + try + { + if (string.IsNullOrEmpty(serverSrcPath)) return; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; + + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = "/usr/bin/pgrep", + Arguments = $"-f \"uv .*--directory {serverSrcPath}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = System.Diagnostics.Process.Start(psi); + if (p == null) return; + string outp = p.StandardOutput.ReadToEnd(); + p.WaitForExit(1500); + if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp)) + { + foreach (var line in outp.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (int.TryParse(line.Trim(), out int pid)) + { + try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { } + } + } + } + } + catch { } + } + + private static string ReadVersionFile(string path) + { + try + { + if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null; + string v = File.ReadAllText(path).Trim(); + return string.IsNullOrEmpty(v) ? null : v; + } + catch { return null; } + } + + private static int CompareSemverSafe(string a, string b) + { + try + { + if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0; + var ap = a.Split('.'); + var bp = b.Split('.'); + for (int i = 0; i < Math.Max(ap.Length, bp.Length); i++) + { + int ai = (i < ap.Length && int.TryParse(ap[i], out var t1)) ? t1 : 0; + int bi = (i < bp.Length && int.TryParse(bp[i], out var t2)) ? t2 : 0; + if (ai != bi) return ai.CompareTo(bi); + } + return 0; + } + catch { return 0; } + } + + /// + /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package + /// or common development locations. + /// + private static bool TryGetEmbeddedServerSource(out string srcPath) + { + return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath); + } + + private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" }; + private static void CopyDirectoryRecursive(string sourceDir, string destinationDir) + { + Directory.CreateDirectory(destinationDir); + + foreach (string filePath in Directory.GetFiles(sourceDir)) + { + string fileName = Path.GetFileName(filePath); + string destFile = Path.Combine(destinationDir, fileName); + File.Copy(filePath, destFile, overwrite: true); + } + + foreach (string dirPath in Directory.GetDirectories(sourceDir)) + { + string dirName = Path.GetFileName(dirPath); + foreach (var skip in _skipDirs) + { + if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase)) + goto NextDir; + } + try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { } + string destSubDir = Path.Combine(destinationDir, dirName); + CopyDirectoryRecursive(dirPath, destSubDir); + NextDir:; + } + } + + public static bool RebuildMcpServer() + { + try + { + // Find embedded source + if (!TryGetEmbeddedServerSource(out string embeddedSrc)) + { + Debug.LogError("RebuildMcpServer: Could not find embedded server source."); + return false; + } + + string saveLocation = GetSaveLocation(); + string destRoot = Path.Combine(saveLocation, ServerFolder); + string destSrc = Path.Combine(destRoot, "src"); + + // Kill any running uv processes for this server + TryKillUvForPath(destSrc); + + // Delete the entire installed server directory + if (Directory.Exists(destRoot)) + { + try + { + Directory.Delete(destRoot, recursive: true); + Debug.Log($"MCP-FOR-UNITY: Deleted existing server at {destRoot}"); + } + catch (Exception ex) + { + Debug.LogError($"Failed to delete existing server: {ex.Message}"); + return false; + } + } + + // Re-copy from embedded source + string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; + Directory.CreateDirectory(destRoot); + CopyDirectoryRecursive(embeddedRoot, destRoot); + + // Write version file + string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown"; + try + { + File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to write version file: {ex.Message}"); + } + + Debug.Log($"MCP-FOR-UNITY: Server rebuilt successfully at {destRoot} (version {embeddedVer})"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"RebuildMcpServer failed: {ex.Message}"); + return false; + } + } + + internal static string FindUvPath() + { + // Allow user override via EditorPrefs + try + { + string overridePath = EditorPrefs.GetString("MCPForUnity.UvPath", string.Empty); + if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath)) + { + if (ValidateUvBinary(overridePath)) return overridePath; + } + } + catch { } + + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + + // Platform-specific candidate lists + string[] candidates; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; + string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty; + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; + + // Fast path: resolve from PATH first + try + { + var wherePsi = new System.Diagnostics.ProcessStartInfo + { + FileName = "where", + Arguments = "uv.exe", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var wp = System.Diagnostics.Process.Start(wherePsi); + string output = wp.StandardOutput.ReadToEnd().Trim(); + wp.WaitForExit(1500); + if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output)) + { + foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + string path = line.Trim(); + if (File.Exists(path) && ValidateUvBinary(path)) return path; + } + } + } + catch { } + + // Windows Store (PythonSoftwareFoundation) install location probe + // Example: %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.13_*\LocalCache\local-packages\Python313\Scripts\uv.exe + try + { + string pkgsRoot = Path.Combine(localAppData, "Packages"); + if (Directory.Exists(pkgsRoot)) + { + var pythonPkgs = Directory.GetDirectories(pkgsRoot, "PythonSoftwareFoundation.Python.*", SearchOption.TopDirectoryOnly) + .OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase); + foreach (var pkg in pythonPkgs) + { + string localCache = Path.Combine(pkg, "LocalCache", "local-packages"); + if (!Directory.Exists(localCache)) continue; + var pyRoots = Directory.GetDirectories(localCache, "Python*", SearchOption.TopDirectoryOnly) + .OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase); + foreach (var pyRoot in pyRoots) + { + string uvExe = Path.Combine(pyRoot, "Scripts", "uv.exe"); + if (File.Exists(uvExe) && ValidateUvBinary(uvExe)) return uvExe; + } + } + } + } + catch { } + + candidates = new[] + { + // Preferred: WinGet Links shims (stable entrypoints) + // Per-user shim (LOCALAPPDATA) → machine-wide shim (Program Files\WinGet\Links) + Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"), + Path.Combine(programFiles, "WinGet", "Links", "uv.exe"), + + // Common per-user installs + Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"), + Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"), + Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"), + Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"), + Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"), + Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"), + Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"), + Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"), + + // Program Files style installs (if a native installer was used) + Path.Combine(programFiles, @"uv\uv.exe"), + + // Try simple name resolution later via PATH + "uv.exe", + "uv" + }; + } + else + { + candidates = new[] + { + "/opt/homebrew/bin/uv", + "/usr/local/bin/uv", + "/usr/bin/uv", + "/opt/local/bin/uv", + Path.Combine(home, ".local", "bin", "uv"), + "/opt/homebrew/opt/uv/bin/uv", + // Framework Python installs + "/Library/Frameworks/Python.framework/Versions/3.13/bin/uv", + "/Library/Frameworks/Python.framework/Versions/3.12/bin/uv", + // Fallback to PATH resolution by name + "uv" + }; + } + + foreach (string c in candidates) + { + try + { + if (File.Exists(c) && ValidateUvBinary(c)) return c; + } + catch { /* ignore */ } + } + + // Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier) + try + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var whichPsi = new System.Diagnostics.ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = "uv", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + try + { + // Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env + string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + string prepend = string.Join(":", new[] + { + System.IO.Path.Combine(homeDir, ".local", "bin"), + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin" + }); + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath); + } + catch { } + using var wp = System.Diagnostics.Process.Start(whichPsi); + string output = wp.StandardOutput.ReadToEnd().Trim(); + wp.WaitForExit(3000); + if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + if (ValidateUvBinary(output)) return output; + } + } + } + catch { } + + // Manual PATH scan + try + { + string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + string[] parts = pathEnv.Split(Path.PathSeparator); + foreach (string part in parts) + { + try + { + // Check both uv and uv.exe + string candidateUv = Path.Combine(part, "uv"); + string candidateUvExe = Path.Combine(part, "uv.exe"); + if (File.Exists(candidateUv) && ValidateUvBinary(candidateUv)) return candidateUv; + if (File.Exists(candidateUvExe) && ValidateUvBinary(candidateUvExe)) return candidateUvExe; + } + catch { } + } + } + catch { } + + return null; + } + + private static bool ValidateUvBinary(string uvPath) + { + try + { + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = System.Diagnostics.Process.Start(psi); + if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; } + if (p.ExitCode == 0) + { + string output = p.StandardOutput.ReadToEnd().Trim(); + return output.StartsWith("uv "); + } + } + catch { } + return false; + } + } +} diff --git a/MCPForUnity/Editor/Helpers/ServerInstaller.cs.meta b/MCPForUnity/Editor/Helpers/ServerInstaller.cs.meta new file mode 100644 index 00000000..dfd9023b --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5862c6a6d0a914f4d83224f8d039cf7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/ServerPathResolver.cs b/MCPForUnity/Editor/Helpers/ServerPathResolver.cs new file mode 100644 index 00000000..0e462945 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ServerPathResolver.cs @@ -0,0 +1,141 @@ +using System; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + public static class ServerPathResolver + { + /// + /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package + /// or common development locations. Returns true if found and sets srcPath to the folder + /// containing server.py. + /// + public static bool TryFindEmbeddedServerSource(out string srcPath) + { + // 1) Repo development layouts commonly used alongside this package + try + { + string projectRoot = Path.GetDirectoryName(Application.dataPath); + string[] devCandidates = + { + Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"), + Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"), + }; + foreach (string candidate in devCandidates) + { + string full = Path.GetFullPath(candidate); + if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py"))) + { + srcPath = full; + return true; + } + } + } + catch { /* ignore */ } + + // 2) Resolve via local package info (no network). Fall back to Client.List on older editors. + try + { +#if UNITY_2021_2_OR_NEWER + // Primary: the package that owns this assembly + var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly); + if (owner != null) + { + if (TryResolveWithinPackage(owner, out srcPath)) + { + return true; + } + } + + // Secondary: scan all registered packages locally + foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages()) + { + if (TryResolveWithinPackage(p, out srcPath)) + { + return true; + } + } +#else + // Older Unity versions: use Package Manager Client.List as a fallback + var list = UnityEditor.PackageManager.Client.List(); + while (!list.IsCompleted) { } + if (list.Status == UnityEditor.PackageManager.StatusCode.Success) + { + foreach (var pkg in list.Result) + { + if (TryResolveWithinPackage(pkg, out srcPath)) + { + return true; + } + } + } +#endif + } + catch { /* ignore */ } + + // 3) Fallback to previous common install locations + try + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + string[] candidates = + { + Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"), + Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"), + }; + foreach (string candidate in candidates) + { + if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py"))) + { + srcPath = candidate; + return true; + } + } + } + catch { /* ignore */ } + + srcPath = null; + return false; + } + + private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath) + { + const string CurrentId = "com.coplaydev.unity-mcp"; + + srcPath = null; + if (p == null || p.name != CurrentId) + { + return false; + } + + string packagePath = p.resolvedPath; + + // Preferred tilde folder (embedded but excluded from import) + string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src"); + if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py"))) + { + srcPath = embeddedTilde; + return true; + } + + // Legacy non-tilde folder + string embedded = Path.Combine(packagePath, "UnityMcpServer", "src"); + if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py"))) + { + srcPath = embedded; + return true; + } + + // Dev-linked sibling of the package folder + string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src"); + if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py"))) + { + srcPath = sibling; + return true; + } + + return false; + } + } +} diff --git a/MCPForUnity/Editor/Helpers/ServerPathResolver.cs.meta b/MCPForUnity/Editor/Helpers/ServerPathResolver.cs.meta new file mode 100644 index 00000000..d02df608 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ServerPathResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Helpers/TelemetryHelper.cs b/MCPForUnity/Editor/Helpers/TelemetryHelper.cs new file mode 100644 index 00000000..6440a675 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/TelemetryHelper.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Unity Bridge telemetry helper for collecting usage analytics + /// Following privacy-first approach with easy opt-out mechanisms + /// + public static class TelemetryHelper + { + private const string TELEMETRY_DISABLED_KEY = "MCPForUnity.TelemetryDisabled"; + private const string CUSTOMER_UUID_KEY = "MCPForUnity.CustomerUUID"; + private static Action> s_sender; + + /// + /// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs) + /// + public static bool IsEnabled + { + get + { + // Check environment variables first + var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY"); + if (!string.IsNullOrEmpty(envDisable) && + (envDisable.ToLower() == "true" || envDisable == "1")) + { + return false; + } + + var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY"); + if (!string.IsNullOrEmpty(unityMcpDisable) && + (unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1")) + { + return false; + } + + // Honor protocol-wide opt-out as well + var mcpDisable = Environment.GetEnvironmentVariable("MCP_DISABLE_TELEMETRY"); + if (!string.IsNullOrEmpty(mcpDisable) && + (mcpDisable.Equals("true", StringComparison.OrdinalIgnoreCase) || mcpDisable == "1")) + { + return false; + } + + // Check EditorPrefs + return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false); + } + } + + /// + /// Get or generate customer UUID for anonymous tracking + /// + public static string GetCustomerUUID() + { + var uuid = UnityEditor.EditorPrefs.GetString(CUSTOMER_UUID_KEY, ""); + if (string.IsNullOrEmpty(uuid)) + { + uuid = System.Guid.NewGuid().ToString(); + UnityEditor.EditorPrefs.SetString(CUSTOMER_UUID_KEY, uuid); + } + return uuid; + } + + /// + /// Disable telemetry (stored in EditorPrefs) + /// + public static void DisableTelemetry() + { + UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true); + } + + /// + /// Enable telemetry (stored in EditorPrefs) + /// + public static void EnableTelemetry() + { + UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false); + } + + /// + /// Send telemetry data to Python server for processing + /// This is a lightweight bridge - the actual telemetry logic is in Python + /// + public static void RecordEvent(string eventType, Dictionary data = null) + { + if (!IsEnabled) + return; + + try + { + var telemetryData = new Dictionary + { + ["event_type"] = eventType, + ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + ["customer_uuid"] = GetCustomerUUID(), + ["unity_version"] = Application.unityVersion, + ["platform"] = Application.platform.ToString(), + ["source"] = "unity_bridge" + }; + + if (data != null) + { + telemetryData["data"] = data; + } + + // Send to Python server via existing bridge communication + // The Python server will handle actual telemetry transmission + SendTelemetryToPythonServer(telemetryData); + } + catch (Exception e) + { + // Never let telemetry errors interfere with functionality + if (IsDebugEnabled()) + { + Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}"); + } + } + } + + /// + /// Allows the bridge to register a concrete sender for telemetry payloads. + /// + public static void RegisterTelemetrySender(Action> sender) + { + Interlocked.Exchange(ref s_sender, sender); + } + + public static void UnregisterTelemetrySender() + { + Interlocked.Exchange(ref s_sender, null); + } + + /// + /// Record bridge startup event + /// + public static void RecordBridgeStartup() + { + RecordEvent("bridge_startup", new Dictionary + { + ["bridge_version"] = "3.0.2", + ["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode() + }); + } + + /// + /// Record bridge connection event + /// + public static void RecordBridgeConnection(bool success, string error = null) + { + var data = new Dictionary + { + ["success"] = success + }; + + if (!string.IsNullOrEmpty(error)) + { + data["error"] = error.Substring(0, Math.Min(200, error.Length)); + } + + RecordEvent("bridge_connection", data); + } + + /// + /// Record tool execution from Unity side + /// + public static void RecordToolExecution(string toolName, bool success, float durationMs, string error = null) + { + var data = new Dictionary + { + ["tool_name"] = toolName, + ["success"] = success, + ["duration_ms"] = Math.Round(durationMs, 2) + }; + + if (!string.IsNullOrEmpty(error)) + { + data["error"] = error.Substring(0, Math.Min(200, error.Length)); + } + + RecordEvent("tool_execution_unity", data); + } + + private static void SendTelemetryToPythonServer(Dictionary telemetryData) + { + var sender = Volatile.Read(ref s_sender); + if (sender != null) + { + try + { + sender(telemetryData); + return; + } + catch (Exception e) + { + if (IsDebugEnabled()) + { + Debug.LogWarning($"Telemetry sender error (non-blocking): {e.Message}"); + } + } + } + + // Fallback: log when debug is enabled + if (IsDebugEnabled()) + { + Debug.Log($"MCP-TELEMETRY: {telemetryData["event_type"]}"); + } + } + + private static bool IsDebugEnabled() + { + try + { + return UnityEditor.EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); + } + catch + { + return false; + } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/TelemetryHelper.cs.meta b/MCPForUnity/Editor/Helpers/TelemetryHelper.cs.meta new file mode 100644 index 00000000..d7fd7b1f --- /dev/null +++ b/MCPForUnity/Editor/Helpers/TelemetryHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b8f3c2d1e7a94f6c8a9b5e3d2c1a0f9e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Helpers/Vector3Helper.cs b/MCPForUnity/Editor/Helpers/Vector3Helper.cs new file mode 100644 index 00000000..41566188 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/Vector3Helper.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Helper class for Vector3 operations + /// + public static class Vector3Helper + { + /// + /// Parses a JArray into a Vector3 + /// + /// The array containing x, y, z coordinates + /// A Vector3 with the parsed coordinates + /// Thrown when array is invalid + public static Vector3 ParseVector3(JArray array) + { + if (array == null || array.Count != 3) + throw new System.Exception("Vector3 must be an array of 3 floats [x, y, z]."); + return new Vector3((float)array[0], (float)array[1], (float)array[2]); + } + } +} diff --git a/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta b/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta new file mode 100644 index 00000000..280381ca --- /dev/null +++ b/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f8514fd42f23cb641a36e52550825b35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/MCPForUnity.Editor.asmdef b/MCPForUnity/Editor/MCPForUnity.Editor.asmdef new file mode 100644 index 00000000..88448922 --- /dev/null +++ b/MCPForUnity/Editor/MCPForUnity.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "MCPForUnity.Editor", + "rootNamespace": "MCPForUnity.Editor", + "references": [ + "MCPForUnity.Runtime", + "GUID:560b04d1a97f54a46a2660c3cc343a6f" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/MCPForUnity/Editor/MCPForUnity.Editor.asmdef.meta b/MCPForUnity/Editor/MCPForUnity.Editor.asmdef.meta new file mode 100644 index 00000000..b819bd4d --- /dev/null +++ b/MCPForUnity/Editor/MCPForUnity.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 98f702da6ca044be59a864a9419c4eab +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/MCPForUnityBridge.cs b/MCPForUnity/Editor/MCPForUnityBridge.cs new file mode 100644 index 00000000..dcc469b0 --- /dev/null +++ b/MCPForUnity/Editor/MCPForUnityBridge.cs @@ -0,0 +1,1194 @@ +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; +using MCPForUnity.Editor.Tools; +using MCPForUnity.Editor.Tools.MenuItems; +using MCPForUnity.Editor.Tools.Prefabs; + +namespace MCPForUnity.Editor +{ + [InitializeOnLoad] + public static partial class MCPForUnityBridge + { + private static TcpListener listener; + private static bool isRunning = false; + private static readonly object lockObj = new(); + private static readonly object startStopLock = new(); + private static readonly object clientsLock = new(); + private static readonly System.Collections.Generic.HashSet activeClients = new(); + // Single-writer outbox for framed responses + private class Outbound + { + public byte[] Payload; + public string Tag; + public int? ReqId; + } + private static readonly BlockingCollection _outbox = new(new ConcurrentQueue()); + private static CancellationTokenSource cts; + private static Task listenerTask; + private static int processingCommands = 0; + private static bool initScheduled = false; + private static bool ensureUpdateHooked = false; + private static bool isStarting = false; + private static double nextStartAt = 0.0f; + private static double nextHeartbeatAt = 0.0f; + private static int heartbeatSeq = 0; + private static Dictionary< + string, + (string commandJson, TaskCompletionSource tcs) + > commandQueue = new(); + private static int mainThreadId; + private static int currentUnityPort = 6400; // Dynamic port, starts with default + private static bool isAutoConnectMode = false; + private const ulong MaxFrameBytes = 64UL * 1024 * 1024; // 64 MiB hard cap for framed payloads + private const int FrameIOTimeoutMs = 30000; // Per-read timeout to avoid stalled clients + + // IO diagnostics + private static long _ioSeq = 0; + private static void IoInfo(string s) { McpLog.Info(s, always: false); } + + // Debug helpers + private static bool IsDebugEnabled() + { + try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); } catch { return false; } + } + + private static void LogBreadcrumb(string stage) + { + if (IsDebugEnabled()) + { + McpLog.Info($"[{stage}]", always: false); + } + } + + public static bool IsRunning => isRunning; + public static int GetCurrentPort() => currentUnityPort; + public static bool IsAutoConnectMode() => isAutoConnectMode; + + /// + /// Start with Auto-Connect mode - discovers new port and saves it + /// + public static void StartAutoConnect() + { + Stop(); // Stop current connection + + try + { + // Prefer stored project port and start using the robust Start() path (with retries/options) + currentUnityPort = PortManager.GetPortWithFallback(); + Start(); + isAutoConnectMode = true; + + // Record telemetry for bridge startup + TelemetryHelper.RecordBridgeStartup(); + } + catch (Exception ex) + { + Debug.LogError($"Auto-connect failed: {ex.Message}"); + + // Record telemetry for connection failure + TelemetryHelper.RecordBridgeConnection(false, ex.Message); + throw; + } + } + + public static bool FolderExists(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + + if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + string fullPath = Path.Combine( + Application.dataPath, + path.StartsWith("Assets/") ? path[7..] : path + ); + return Directory.Exists(fullPath); + } + + static MCPForUnityBridge() + { + // Record the main thread ID for safe thread checks + try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; } + // Start single writer thread for framed responses + try + { + var writerThread = new Thread(() => + { + foreach (var item in _outbox.GetConsumingEnumerable()) + { + try + { + long seq = Interlocked.Increment(ref _ioSeq); + IoInfo($"[IO] ➜ write start seq={seq} tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")}"); + var sw = System.Diagnostics.Stopwatch.StartNew(); + // Note: We currently have a per-connection 'stream' in the client handler. For simplicity, + // writes are performed inline there. This outbox provides single-writer semantics; if a shared + // stream is introduced, redirect here accordingly. + // No-op: actual write happens in client loop using WriteFrameAsync + sw.Stop(); + IoInfo($"[IO] ✓ write end tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")} durMs={sw.Elapsed.TotalMilliseconds:F1}"); + } + catch (Exception ex) + { + IoInfo($"[IO] ✗ write FAIL tag={item.Tag} reqId={(item.ReqId?.ToString() ?? "?")} {ex.GetType().Name}: {ex.Message}"); + } + } + }) + { IsBackground = true, Name = "MCP-Writer" }; + writerThread.Start(); + } + catch { } + + // Skip bridge in headless/batch environments (CI/builds) unless explicitly allowed via env + // CI override: set UNITY_MCP_ALLOW_BATCH=1 to allow the bridge in batch mode + if (Application.isBatchMode && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH"))) + { + return; + } + // Defer start until the editor is idle and not compiling + ScheduleInitRetry(); + // Add a safety net update hook in case delayCall is missed during reload churn + if (!ensureUpdateHooked) + { + ensureUpdateHooked = true; + EditorApplication.update += EnsureStartedOnEditorIdle; + } + EditorApplication.quitting += Stop; + AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; + AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; + // Also coalesce play mode transitions into a deferred init + EditorApplication.playModeStateChanged += _ => ScheduleInitRetry(); + } + + /// + /// Initialize the MCP bridge after Unity is fully loaded and compilation is complete. + /// This prevents repeated restarts during script compilation that cause port hopping. + /// + private static void InitializeAfterCompilation() + { + initScheduled = false; + + // Play-mode friendly: allow starting in play mode; only defer while compiling + if (IsCompiling()) + { + ScheduleInitRetry(); + return; + } + + if (!isRunning) + { + Start(); + if (!isRunning) + { + // If a race prevented start, retry later + ScheduleInitRetry(); + } + } + } + + private static void ScheduleInitRetry() + { + if (initScheduled) + { + return; + } + initScheduled = true; + // Debounce: start ~200ms after the last trigger + nextStartAt = EditorApplication.timeSinceStartup + 0.20f; + // Ensure the update pump is active + if (!ensureUpdateHooked) + { + ensureUpdateHooked = true; + EditorApplication.update += EnsureStartedOnEditorIdle; + } + // Keep the original delayCall as a secondary path + EditorApplication.delayCall += InitializeAfterCompilation; + } + + // Safety net: ensure the bridge starts shortly after domain reload when editor is idle + private static void EnsureStartedOnEditorIdle() + { + // Do nothing while compiling + if (IsCompiling()) + { + return; + } + + // If already running, remove the hook + if (isRunning) + { + EditorApplication.update -= EnsureStartedOnEditorIdle; + ensureUpdateHooked = false; + return; + } + + // Debounced start: wait until the scheduled time + if (nextStartAt > 0 && EditorApplication.timeSinceStartup < nextStartAt) + { + return; + } + + if (isStarting) + { + return; + } + + isStarting = true; + try + { + // Attempt start; if it succeeds, remove the hook to avoid overhead + Start(); + } + finally + { + isStarting = false; + } + if (isRunning) + { + EditorApplication.update -= EnsureStartedOnEditorIdle; + ensureUpdateHooked = false; + } + } + + // Helper to check compilation status across Unity versions + private static bool IsCompiling() + { + if (EditorApplication.isCompiling) + { + return true; + } + try + { + System.Type pipeline = System.Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor"); + var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + if (prop != null) + { + return (bool)prop.GetValue(null); + } + } + catch { } + return false; + } + + public static void Start() + { + lock (startStopLock) + { + // Don't restart if already running on a working port + if (isRunning && listener != null) + { + if (IsDebugEnabled()) + { + Debug.Log($"MCP-FOR-UNITY: MCPForUnityBridge already running on port {currentUnityPort}"); + } + return; + } + + Stop(); + + // Attempt fast bind with stored-port preference (sticky per-project) + try + { + // Always consult PortManager first so we prefer the persisted project port + currentUnityPort = PortManager.GetPortWithFallback(); + + // Breadcrumb: Start + LogBreadcrumb("Start"); + + const int maxImmediateRetries = 3; + const int retrySleepMs = 75; + int attempt = 0; + for (; ; ) + { + try + { + listener = new TcpListener(IPAddress.Loopback, currentUnityPort); + listener.Server.SetSocketOption( + SocketOptionLevel.Socket, + SocketOptionName.ReuseAddress, + true + ); +#if UNITY_EDITOR_WIN + try + { + listener.ExclusiveAddressUse = false; + } + catch { } +#endif + // Minimize TIME_WAIT by sending RST on close + try + { + listener.Server.LingerState = new LingerOption(true, 0); + } + catch (Exception) + { + // Ignore if not supported on platform + } + listener.Start(); + break; + } + catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse && attempt < maxImmediateRetries) + { + attempt++; + Thread.Sleep(retrySleepMs); + continue; + } + catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse && attempt >= maxImmediateRetries) + { + currentUnityPort = PortManager.GetPortWithFallback(); + listener = new TcpListener(IPAddress.Loopback, currentUnityPort); + listener.Server.SetSocketOption( + SocketOptionLevel.Socket, + SocketOptionName.ReuseAddress, + true + ); +#if UNITY_EDITOR_WIN + try + { + listener.ExclusiveAddressUse = false; + } + catch { } +#endif + try + { + listener.Server.LingerState = new LingerOption(true, 0); + } + catch (Exception) + { + } + listener.Start(); + break; + } + } + + isRunning = true; + isAutoConnectMode = false; + string platform = Application.platform.ToString(); + string serverVer = ReadInstalledServerVersionSafe(); + Debug.Log($"MCP-FOR-UNITY: MCPForUnityBridge started on port {currentUnityPort}. (OS={platform}, server={serverVer})"); + // Start background listener with cooperative cancellation + cts = new CancellationTokenSource(); + listenerTask = Task.Run(() => ListenerLoopAsync(cts.Token)); + CommandRegistry.Initialize(); + EditorApplication.update += ProcessCommands; + // Ensure lifecycle events are (re)subscribed in case Stop() removed them earlier in-domain + try { AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; } catch { } + try { AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; } catch { } + try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { } + try { AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; } catch { } + try { EditorApplication.quitting -= Stop; } catch { } + try { EditorApplication.quitting += Stop; } catch { } + // Write initial heartbeat immediately + heartbeatSeq++; + WriteHeartbeat(false, "ready"); + nextHeartbeatAt = EditorApplication.timeSinceStartup + 0.5f; + } + catch (SocketException ex) + { + Debug.LogError($"Failed to start TCP listener: {ex.Message}"); + } + } + } + + public static void Stop() + { + Task toWait = null; + lock (startStopLock) + { + if (!isRunning) + { + return; + } + + try + { + // Mark as stopping early to avoid accept logging during disposal + isRunning = false; + + // Quiesce background listener quickly + var cancel = cts; + cts = null; + try { cancel?.Cancel(); } catch { } + + try { listener?.Stop(); } catch { } + listener = null; + + // Capture background task to wait briefly outside the lock + toWait = listenerTask; + listenerTask = null; + } + catch (Exception ex) + { + Debug.LogError($"Error stopping MCPForUnityBridge: {ex.Message}"); + } + } + + // Proactively close all active client sockets to unblock any pending reads + TcpClient[] toClose; + lock (clientsLock) + { + toClose = activeClients.ToArray(); + activeClients.Clear(); + } + foreach (var c in toClose) + { + try { c.Close(); } catch { } + } + + // Give the background loop a short window to exit without blocking the editor + if (toWait != null) + { + try { toWait.Wait(100); } catch { } + } + + // Now unhook editor events safely + try { EditorApplication.update -= ProcessCommands; } catch { } + try { AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; } catch { } + try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { } + try { EditorApplication.quitting -= Stop; } catch { } + + if (IsDebugEnabled()) Debug.Log("MCP-FOR-UNITY: MCPForUnityBridge stopped."); + } + + private static async Task ListenerLoopAsync(CancellationToken token) + { + while (isRunning && !token.IsCancellationRequested) + { + try + { + TcpClient client = await listener.AcceptTcpClientAsync(); + // Enable basic socket keepalive + client.Client.SetSocketOption( + SocketOptionLevel.Socket, + SocketOptionName.KeepAlive, + true + ); + + // Set longer receive timeout to prevent quick disconnections + client.ReceiveTimeout = 60000; // 60 seconds + + // Fire and forget each client connection + _ = Task.Run(() => HandleClientAsync(client, token), token); + } + catch (ObjectDisposedException) + { + // Listener was disposed during stop/reload; exit quietly + if (!isRunning || token.IsCancellationRequested) + { + break; + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + if (isRunning && !token.IsCancellationRequested) + { + if (IsDebugEnabled()) Debug.LogError($"Listener error: {ex.Message}"); + } + } + } + } + + private static async Task HandleClientAsync(TcpClient client, CancellationToken token) + { + using (client) + using (NetworkStream stream = client.GetStream()) + { + lock (clientsLock) { activeClients.Add(client); } + try + { + // Framed I/O only; legacy mode removed + try + { + if (IsDebugEnabled()) + { + var ep = client.Client?.RemoteEndPoint?.ToString() ?? "unknown"; + Debug.Log($"UNITY-MCP: Client connected {ep}"); + } + } + catch { } + // Strict framing: always require FRAMING=1 and frame all I/O + try + { + client.NoDelay = true; + } + catch { } + try + { + string handshake = "WELCOME UNITY-MCP 1 FRAMING=1\n"; + byte[] handshakeBytes = System.Text.Encoding.ASCII.GetBytes(handshake); + using var cts = new CancellationTokenSource(FrameIOTimeoutMs); +#if NETSTANDARD2_1 || NET6_0_OR_GREATER + await stream.WriteAsync(handshakeBytes.AsMemory(0, handshakeBytes.Length), cts.Token).ConfigureAwait(false); +#else + await stream.WriteAsync(handshakeBytes, 0, handshakeBytes.Length, cts.Token).ConfigureAwait(false); +#endif + if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info("Sent handshake FRAMING=1 (strict)", always: false); + } + catch (Exception ex) + { + if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Warn($"Handshake failed: {ex.Message}"); + return; // abort this client + } + + while (isRunning && !token.IsCancellationRequested) + { + try + { + // Strict framed mode only: enforced framed I/O for this connection + string commandText = await ReadFrameAsUtf8Async(stream, FrameIOTimeoutMs, token).ConfigureAwait(false); + + try + { + if (IsDebugEnabled()) + { + var preview = commandText.Length > 120 ? commandText.Substring(0, 120) + "…" : commandText; + MCPForUnity.Editor.Helpers.McpLog.Info($"recv framed: {preview}", always: false); + } + } + catch { } + string commandId = Guid.NewGuid().ToString(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Special handling for ping command to avoid JSON parsing + if (commandText.Trim() == "ping") + { + // Direct response to ping without going through JSON parsing + byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes( + /*lang=json,strict*/ + "{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}" + ); + await WriteFrameAsync(stream, pingResponseBytes); + continue; + } + + lock (lockObj) + { + commandQueue[commandId] = (commandText, tcs); + } + + // Wait for the handler to produce a response, but do not block indefinitely + string response; + try + { + using var respCts = new CancellationTokenSource(FrameIOTimeoutMs); + var completed = await Task.WhenAny(tcs.Task, Task.Delay(FrameIOTimeoutMs, respCts.Token)).ConfigureAwait(false); + if (completed == tcs.Task) + { + // Got a result from the handler + respCts.Cancel(); + response = tcs.Task.Result; + } + else + { + // Timeout: return a structured error so the client can recover + var timeoutResponse = new + { + status = "error", + error = $"Command processing timed out after {FrameIOTimeoutMs} ms", + }; + response = JsonConvert.SerializeObject(timeoutResponse); + } + } + catch (Exception ex) + { + var errorResponse = new + { + status = "error", + error = ex.Message, + }; + response = JsonConvert.SerializeObject(errorResponse); + } + + if (IsDebugEnabled()) + { + try { MCPForUnity.Editor.Helpers.McpLog.Info("[MCP] sending framed response", always: false); } catch { } + } + // Crash-proof and self-reporting writer logs (direct write to this client's stream) + long seq = System.Threading.Interlocked.Increment(ref _ioSeq); + byte[] responseBytes; + try + { + responseBytes = System.Text.Encoding.UTF8.GetBytes(response); + IoInfo($"[IO] ➜ write start seq={seq} tag=response len={responseBytes.Length} reqId=?"); + } + catch (Exception ex) + { + IoInfo($"[IO] ✗ serialize FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}"); + throw; + } + + var swDirect = System.Diagnostics.Stopwatch.StartNew(); + try + { + await WriteFrameAsync(stream, responseBytes); + swDirect.Stop(); + IoInfo($"[IO] ✓ write end tag=response len={responseBytes.Length} reqId=? durMs={swDirect.Elapsed.TotalMilliseconds:F1}"); + } + catch (Exception ex) + { + IoInfo($"[IO] ✗ write FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}"); + throw; + } + } + catch (Exception ex) + { + // Treat common disconnects/timeouts as benign; only surface hard errors + string msg = ex.Message ?? string.Empty; + bool isBenign = + msg.IndexOf("Connection closed before reading expected bytes", StringComparison.OrdinalIgnoreCase) >= 0 + || msg.IndexOf("Read timed out", StringComparison.OrdinalIgnoreCase) >= 0 + || ex is System.IO.IOException; + if (isBenign) + { + if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info($"Client handler: {msg}", always: false); + } + else + { + MCPForUnity.Editor.Helpers.McpLog.Error($"Client handler error: {msg}"); + } + break; + } + } + } + finally + { + lock (clientsLock) { activeClients.Remove(client); } + } + } + } + + // Timeout-aware exact read helper with cancellation; avoids indefinite stalls and background task leaks + private static async System.Threading.Tasks.Task ReadExactAsync(NetworkStream stream, int count, int timeoutMs, CancellationToken cancel = default) + { + byte[] buffer = new byte[count]; + int offset = 0; + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + while (offset < count) + { + int remaining = count - offset; + int remainingTimeout = timeoutMs <= 0 + ? Timeout.Infinite + : timeoutMs - (int)stopwatch.ElapsedMilliseconds; + + // If a finite timeout is configured and already elapsed, fail immediately + if (remainingTimeout != Timeout.Infinite && remainingTimeout <= 0) + { + throw new System.IO.IOException("Read timed out"); + } + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancel); + if (remainingTimeout != Timeout.Infinite) + { + cts.CancelAfter(remainingTimeout); + } + + try + { +#if NETSTANDARD2_1 || NET6_0_OR_GREATER + int read = await stream.ReadAsync(buffer.AsMemory(offset, remaining), cts.Token).ConfigureAwait(false); +#else + int read = await stream.ReadAsync(buffer, offset, remaining, cts.Token).ConfigureAwait(false); +#endif + if (read == 0) + { + throw new System.IO.IOException("Connection closed before reading expected bytes"); + } + offset += read; + } + catch (OperationCanceledException) when (!cancel.IsCancellationRequested) + { + throw new System.IO.IOException("Read timed out"); + } + } + + return buffer; + } + + private static async System.Threading.Tasks.Task WriteFrameAsync(NetworkStream stream, byte[] payload) + { + using var cts = new CancellationTokenSource(FrameIOTimeoutMs); + await WriteFrameAsync(stream, payload, cts.Token); + } + + private static async System.Threading.Tasks.Task WriteFrameAsync(NetworkStream stream, byte[] payload, CancellationToken cancel) + { + if (payload == null) + { + throw new System.ArgumentNullException(nameof(payload)); + } + if ((ulong)payload.LongLength > MaxFrameBytes) + { + throw new System.IO.IOException($"Frame too large: {payload.LongLength}"); + } + byte[] header = new byte[8]; + WriteUInt64BigEndian(header, (ulong)payload.LongLength); +#if NETSTANDARD2_1 || NET6_0_OR_GREATER + await stream.WriteAsync(header.AsMemory(0, header.Length), cancel).ConfigureAwait(false); + await stream.WriteAsync(payload.AsMemory(0, payload.Length), cancel).ConfigureAwait(false); +#else + await stream.WriteAsync(header, 0, header.Length, cancel).ConfigureAwait(false); + await stream.WriteAsync(payload, 0, payload.Length, cancel).ConfigureAwait(false); +#endif + } + + private static async System.Threading.Tasks.Task ReadFrameAsUtf8Async(NetworkStream stream, int timeoutMs, CancellationToken cancel) + { + byte[] header = await ReadExactAsync(stream, 8, timeoutMs, cancel).ConfigureAwait(false); + ulong payloadLen = ReadUInt64BigEndian(header); + if (payloadLen > MaxFrameBytes) + { + throw new System.IO.IOException($"Invalid framed length: {payloadLen}"); + } + if (payloadLen == 0UL) + throw new System.IO.IOException("Zero-length frames are not allowed"); + if (payloadLen > int.MaxValue) + { + throw new System.IO.IOException("Frame too large for buffer"); + } + int count = (int)payloadLen; + byte[] payload = await ReadExactAsync(stream, count, timeoutMs, cancel).ConfigureAwait(false); + return System.Text.Encoding.UTF8.GetString(payload); + } + + private static ulong ReadUInt64BigEndian(byte[] buffer) + { + if (buffer == null || buffer.Length < 8) return 0UL; + return ((ulong)buffer[0] << 56) + | ((ulong)buffer[1] << 48) + | ((ulong)buffer[2] << 40) + | ((ulong)buffer[3] << 32) + | ((ulong)buffer[4] << 24) + | ((ulong)buffer[5] << 16) + | ((ulong)buffer[6] << 8) + | buffer[7]; + } + + private static void WriteUInt64BigEndian(byte[] dest, ulong value) + { + if (dest == null || dest.Length < 8) + { + throw new System.ArgumentException("Destination buffer too small for UInt64"); + } + dest[0] = (byte)(value >> 56); + dest[1] = (byte)(value >> 48); + dest[2] = (byte)(value >> 40); + dest[3] = (byte)(value >> 32); + dest[4] = (byte)(value >> 24); + dest[5] = (byte)(value >> 16); + dest[6] = (byte)(value >> 8); + dest[7] = (byte)(value); + } + + private static void ProcessCommands() + { + if (!isRunning) return; + if (Interlocked.Exchange(ref processingCommands, 1) == 1) return; // reentrancy guard + try + { + // Heartbeat without holding the queue lock + double now = EditorApplication.timeSinceStartup; + if (now >= nextHeartbeatAt) + { + WriteHeartbeat(false); + nextHeartbeatAt = now + 0.5f; + } + + // Snapshot under lock, then process outside to reduce contention + List<(string id, string text, TaskCompletionSource tcs)> work; + lock (lockObj) + { + work = commandQueue + .Select(kvp => (kvp.Key, kvp.Value.commandJson, kvp.Value.tcs)) + .ToList(); + } + + foreach (var item in work) + { + string id = item.id; + string commandText = item.text; + TaskCompletionSource tcs = item.tcs; + + try + { + // Special case handling + if (string.IsNullOrEmpty(commandText)) + { + var emptyResponse = new + { + status = "error", + error = "Empty command received", + }; + tcs.SetResult(JsonConvert.SerializeObject(emptyResponse)); + // Remove quickly under lock + lock (lockObj) { commandQueue.Remove(id); } + continue; + } + + // Trim the command text to remove any whitespace + commandText = commandText.Trim(); + + // Non-JSON direct commands handling (like ping) + if (commandText == "ping") + { + var pingResponse = new + { + status = "success", + result = new { message = "pong" }, + }; + tcs.SetResult(JsonConvert.SerializeObject(pingResponse)); + lock (lockObj) { commandQueue.Remove(id); } + continue; + } + + // Check if the command is valid JSON before attempting to deserialize + if (!IsValidJson(commandText)) + { + var invalidJsonResponse = new + { + status = "error", + error = "Invalid JSON format", + receivedText = commandText.Length > 50 + ? commandText[..50] + "..." + : commandText, + }; + tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse)); + lock (lockObj) { commandQueue.Remove(id); } + continue; + } + + // Normal JSON command processing + Command command = JsonConvert.DeserializeObject(commandText); + + if (command == null) + { + var nullCommandResponse = new + { + status = "error", + error = "Command deserialized to null", + details = "The command was valid JSON but could not be deserialized to a Command object", + }; + tcs.SetResult(JsonConvert.SerializeObject(nullCommandResponse)); + } + else + { + string responseJson = ExecuteCommand(command); + tcs.SetResult(responseJson); + } + } + catch (Exception ex) + { + Debug.LogError($"Error processing command: {ex.Message}\n{ex.StackTrace}"); + + var response = new + { + status = "error", + error = ex.Message, + commandType = "Unknown (error during processing)", + receivedText = commandText?.Length > 50 + ? commandText[..50] + "..." + : commandText, + }; + string responseJson = JsonConvert.SerializeObject(response); + tcs.SetResult(responseJson); + } + + // Remove quickly under lock + lock (lockObj) { commandQueue.Remove(id); } + } + } + finally + { + Interlocked.Exchange(ref processingCommands, 0); + } + } + + // Invoke the given function on the Unity main thread and wait up to timeoutMs for the result. + // Returns null on timeout or error; caller should provide a fallback error response. + private static object InvokeOnMainThreadWithTimeout(Func func, int timeoutMs) + { + if (func == null) return null; + try + { + // If mainThreadId is unknown, assume we're on main thread to avoid blocking the editor. + if (mainThreadId == 0) + { + try { return func(); } + catch (Exception ex) { throw new InvalidOperationException($"Main thread handler error: {ex.Message}", ex); } + } + // If we are already on the main thread, execute directly to avoid deadlocks + try + { + if (Thread.CurrentThread.ManagedThreadId == mainThreadId) + { + return func(); + } + } + catch { } + + object result = null; + Exception captured = null; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + EditorApplication.delayCall += () => + { + try + { + result = func(); + } + catch (Exception ex) + { + captured = ex; + } + finally + { + try { tcs.TrySetResult(true); } catch { } + } + }; + + // Wait for completion with timeout (Editor thread will pump delayCall) + bool completed = tcs.Task.Wait(timeoutMs); + if (!completed) + { + return null; // timeout + } + if (captured != null) + { + throw new InvalidOperationException($"Main thread handler error: {captured.Message}", captured); + } + return result; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to invoke on main thread: {ex.Message}", ex); + } + } + + // Helper method to check if a string is valid JSON + private static bool IsValidJson(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + text = text.Trim(); + if ( + (text.StartsWith("{") && text.EndsWith("}")) + || // Object + (text.StartsWith("[") && text.EndsWith("]")) + ) // Array + { + try + { + JToken.Parse(text); + return true; + } + catch + { + return false; + } + } + + return false; + } + + private static string ExecuteCommand(Command command) + { + try + { + if (string.IsNullOrEmpty(command.type)) + { + var errorResponse = new + { + status = "error", + error = "Command type cannot be empty", + details = "A valid command type is required for processing", + }; + return JsonConvert.SerializeObject(errorResponse); + } + + // Handle ping command for connection verification + if (command.type.Equals("ping", StringComparison.OrdinalIgnoreCase)) + { + var pingResponse = new + { + status = "success", + result = new { message = "pong" }, + }; + return JsonConvert.SerializeObject(pingResponse); + } + + // Use JObject for parameters as the new handlers likely expect this + JObject paramsObject = command.@params ?? new JObject(); + object result = CommandRegistry.GetHandler(command.type)(paramsObject); + + // Standard success response format + var response = new { status = "success", result }; + return JsonConvert.SerializeObject(response); + } + catch (Exception ex) + { + // Log the detailed error in Unity for debugging + Debug.LogError( + $"Error executing command '{command?.type ?? "Unknown"}': {ex.Message}\n{ex.StackTrace}" + ); + + // Standard error response format + var response = new + { + status = "error", + error = ex.Message, // Provide the specific error message + command = command?.type ?? "Unknown", // Include the command type if available + stackTrace = ex.StackTrace, // Include stack trace for detailed debugging + paramsSummary = command?.@params != null + ? GetParamsSummary(command.@params) + : "No parameters", // Summarize parameters for context + }; + return JsonConvert.SerializeObject(response); + } + } + + private static object HandleManageScene(JObject paramsObject) + { + try + { + if (IsDebugEnabled()) Debug.Log("[MCP] manage_scene: dispatching to main thread"); + var sw = System.Diagnostics.Stopwatch.StartNew(); + var r = InvokeOnMainThreadWithTimeout(() => ManageScene.HandleCommand(paramsObject), FrameIOTimeoutMs); + sw.Stop(); + if (IsDebugEnabled()) Debug.Log($"[MCP] manage_scene: completed in {sw.ElapsedMilliseconds} ms"); + return r ?? Response.Error("manage_scene returned null (timeout or error)"); + } + catch (Exception ex) + { + return Response.Error($"manage_scene dispatch error: {ex.Message}"); + } + } + + // Helper method to get a summary of parameters for error reporting + private static string GetParamsSummary(JObject @params) + { + try + { + return @params == null || !@params.HasValues + ? "No parameters" + : string.Join( + ", ", + @params + .Properties() + .Select(static p => + $"{p.Name}: {p.Value?.ToString()?[..Math.Min(20, p.Value?.ToString()?.Length ?? 0)]}" + ) + ); + } + catch + { + return "Could not summarize parameters"; + } + } + + // Heartbeat/status helpers + private static void OnBeforeAssemblyReload() + { + // Stop cleanly before reload so sockets close and clients see 'reloading' + try { Stop(); } catch { } + // Avoid file I/O or heavy work here + } + + private static void OnAfterAssemblyReload() + { + // Will be overwritten by Start(), but mark as alive quickly + WriteHeartbeat(false, "idle"); + LogBreadcrumb("Idle"); + // Schedule a safe restart after reload to avoid races during compilation + ScheduleInitRetry(); + } + + private static void WriteHeartbeat(bool reloading, string reason = null) + { + try + { + // Allow override of status directory (useful in CI/containers) + string dir = Environment.GetEnvironmentVariable("UNITY_MCP_STATUS_DIR"); + if (string.IsNullOrWhiteSpace(dir)) + { + dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp"); + } + Directory.CreateDirectory(dir); + string filePath = Path.Combine(dir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json"); + var payload = new + { + unity_port = currentUnityPort, + reloading, + reason = reason ?? (reloading ? "reloading" : "ready"), + seq = heartbeatSeq, + project_path = Application.dataPath, + last_heartbeat = DateTime.UtcNow.ToString("O") + }; + File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false)); + } + catch (Exception) + { + // Best-effort only + } + } + + private static string ReadInstalledServerVersionSafe() + { + try + { + string serverSrc = ServerInstaller.GetServerPath(); + string verFile = Path.Combine(serverSrc, "server_version.txt"); + if (File.Exists(verFile)) + { + string v = File.ReadAllText(verFile)?.Trim(); + if (!string.IsNullOrEmpty(v)) return v; + } + } + catch { } + return "unknown"; + } + + private static string ComputeProjectHash(string input) + { + try + { + using var sha1 = System.Security.Cryptography.SHA1.Create(); + byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input ?? string.Empty); + byte[] hashBytes = sha1.ComputeHash(bytes); + var sb = new System.Text.StringBuilder(); + foreach (byte b in hashBytes) + { + sb.Append(b.ToString("x2")); + } + return sb.ToString()[..8]; + } + catch + { + return "default"; + } + } + } +} diff --git a/MCPForUnity/Editor/MCPForUnityBridge.cs.meta b/MCPForUnity/Editor/MCPForUnityBridge.cs.meta new file mode 100644 index 00000000..f8d1f46e --- /dev/null +++ b/MCPForUnity/Editor/MCPForUnityBridge.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96dc847eb7f7a45e0b91241db934a4be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models.meta b/MCPForUnity/Editor/Models.meta new file mode 100644 index 00000000..85404561 --- /dev/null +++ b/MCPForUnity/Editor/Models.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 16d3ab36890b6c14f9afeabee30e03e3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models/Command.cs b/MCPForUnity/Editor/Models/Command.cs new file mode 100644 index 00000000..02a89d88 --- /dev/null +++ b/MCPForUnity/Editor/Models/Command.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Linq; + +namespace MCPForUnity.Editor.Models +{ + /// + /// Represents a command received from the MCP client + /// + public class Command + { + /// + /// The type of command to execute + /// + public string type { get; set; } + + /// + /// The parameters for the command + /// + public JObject @params { get; set; } + } +} + diff --git a/MCPForUnity/Editor/Models/Command.cs.meta b/MCPForUnity/Editor/Models/Command.cs.meta new file mode 100644 index 00000000..63618f53 --- /dev/null +++ b/MCPForUnity/Editor/Models/Command.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6754c84e5deb74749bc3a19e0c9aa280 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models/MCPConfigServer.cs b/MCPForUnity/Editor/Models/MCPConfigServer.cs new file mode 100644 index 00000000..fbffed37 --- /dev/null +++ b/MCPForUnity/Editor/Models/MCPConfigServer.cs @@ -0,0 +1,19 @@ +using System; +using Newtonsoft.Json; + +namespace MCPForUnity.Editor.Models +{ + [Serializable] + public class McpConfigServer + { + [JsonProperty("command")] + public string command; + + [JsonProperty("args")] + public string[] args; + + // VSCode expects a transport type; include only when explicitly set + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string type; + } +} diff --git a/MCPForUnity/Editor/Models/MCPConfigServer.cs.meta b/MCPForUnity/Editor/Models/MCPConfigServer.cs.meta new file mode 100644 index 00000000..0574c5a6 --- /dev/null +++ b/MCPForUnity/Editor/Models/MCPConfigServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5fae9d995f514e9498e9613e2cdbeca9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models/MCPConfigServers.cs b/MCPForUnity/Editor/Models/MCPConfigServers.cs new file mode 100644 index 00000000..d5065a16 --- /dev/null +++ b/MCPForUnity/Editor/Models/MCPConfigServers.cs @@ -0,0 +1,12 @@ +using System; +using Newtonsoft.Json; + +namespace MCPForUnity.Editor.Models +{ + [Serializable] + public class McpConfigServers + { + [JsonProperty("unityMCP")] + public McpConfigServer unityMCP; + } +} diff --git a/MCPForUnity/Editor/Models/MCPConfigServers.cs.meta b/MCPForUnity/Editor/Models/MCPConfigServers.cs.meta new file mode 100644 index 00000000..1fb5f0b2 --- /dev/null +++ b/MCPForUnity/Editor/Models/MCPConfigServers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bcb583553e8173b49be71a5c43bd9502 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models/McpClient.cs b/MCPForUnity/Editor/Models/McpClient.cs new file mode 100644 index 00000000..a32f7f59 --- /dev/null +++ b/MCPForUnity/Editor/Models/McpClient.cs @@ -0,0 +1,47 @@ +namespace MCPForUnity.Editor.Models +{ + public class McpClient + { + public string name; + public string windowsConfigPath; + public string macConfigPath; + public string linuxConfigPath; + public McpTypes mcpType; + public string configStatus; + public McpStatus status = McpStatus.NotConfigured; + + // Helper method to convert the enum to a display string + public string GetStatusDisplayString() + { + return status switch + { + McpStatus.NotConfigured => "Not Configured", + McpStatus.Configured => "Configured", + McpStatus.Running => "Running", + McpStatus.Connected => "Connected", + McpStatus.IncorrectPath => "Incorrect Path", + McpStatus.CommunicationError => "Communication Error", + McpStatus.NoResponse => "No Response", + McpStatus.UnsupportedOS => "Unsupported OS", + McpStatus.MissingConfig => "Missing MCPForUnity Config", + McpStatus.Error => configStatus.StartsWith("Error:") ? configStatus : "Error", + _ => "Unknown", + }; + } + + // Helper method to set both status enum and string for backward compatibility + public void SetStatus(McpStatus newStatus, string errorDetails = null) + { + status = newStatus; + + if (newStatus == McpStatus.Error && !string.IsNullOrEmpty(errorDetails)) + { + configStatus = $"Error: {errorDetails}"; + } + else + { + configStatus = GetStatusDisplayString(); + } + } + } +} diff --git a/MCPForUnity/Editor/Models/McpClient.cs.meta b/MCPForUnity/Editor/Models/McpClient.cs.meta new file mode 100644 index 00000000..b08dcf3b --- /dev/null +++ b/MCPForUnity/Editor/Models/McpClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b1afa56984aec0d41808edcebf805e6a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models/McpConfig.cs b/MCPForUnity/Editor/Models/McpConfig.cs new file mode 100644 index 00000000..9ddf9d09 --- /dev/null +++ b/MCPForUnity/Editor/Models/McpConfig.cs @@ -0,0 +1,12 @@ +using System; +using Newtonsoft.Json; + +namespace MCPForUnity.Editor.Models +{ + [Serializable] + public class McpConfig + { + [JsonProperty("mcpServers")] + public McpConfigServers mcpServers; + } +} diff --git a/MCPForUnity/Editor/Models/McpConfig.cs.meta b/MCPForUnity/Editor/Models/McpConfig.cs.meta new file mode 100644 index 00000000..2a407c31 --- /dev/null +++ b/MCPForUnity/Editor/Models/McpConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c17c09908f0c1524daa8b6957ce1f7f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models/McpStatus.cs b/MCPForUnity/Editor/Models/McpStatus.cs new file mode 100644 index 00000000..d041667d --- /dev/null +++ b/MCPForUnity/Editor/Models/McpStatus.cs @@ -0,0 +1,18 @@ +namespace MCPForUnity.Editor.Models +{ + // Enum representing the various status states for MCP clients + public enum McpStatus + { + NotConfigured, // Not set up yet + Configured, // Successfully configured + Running, // Service is running + Connected, // Successfully connected + IncorrectPath, // Configuration has incorrect paths + CommunicationError, // Connected but communication issues + NoResponse, // Connected but not responding + MissingConfig, // Config file exists but missing required elements + UnsupportedOS, // OS is not supported + Error, // General error state + } +} + diff --git a/MCPForUnity/Editor/Models/McpStatus.cs.meta b/MCPForUnity/Editor/Models/McpStatus.cs.meta new file mode 100644 index 00000000..e8e930d3 --- /dev/null +++ b/MCPForUnity/Editor/Models/McpStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa63057c9e5282d4887352578bf49971 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models/McpTypes.cs b/MCPForUnity/Editor/Models/McpTypes.cs new file mode 100644 index 00000000..a5a03dec --- /dev/null +++ b/MCPForUnity/Editor/Models/McpTypes.cs @@ -0,0 +1,13 @@ +namespace MCPForUnity.Editor.Models +{ + public enum McpTypes + { + ClaudeCode, + ClaudeDesktop, + Codex, + Cursor, + Kiro, + VSCode, + Windsurf, + } +} diff --git a/MCPForUnity/Editor/Models/McpTypes.cs.meta b/MCPForUnity/Editor/Models/McpTypes.cs.meta new file mode 100644 index 00000000..377a6d0b --- /dev/null +++ b/MCPForUnity/Editor/Models/McpTypes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Models/ServerConfig.cs b/MCPForUnity/Editor/Models/ServerConfig.cs new file mode 100644 index 00000000..4b185f1f --- /dev/null +++ b/MCPForUnity/Editor/Models/ServerConfig.cs @@ -0,0 +1,36 @@ +using System; +using Newtonsoft.Json; + +namespace MCPForUnity.Editor.Models +{ + [Serializable] + public class ServerConfig + { + [JsonProperty("unity_host")] + public string unityHost = "localhost"; + + [JsonProperty("unity_port")] + public int unityPort; + + [JsonProperty("mcp_port")] + public int mcpPort; + + [JsonProperty("connection_timeout")] + public float connectionTimeout; + + [JsonProperty("buffer_size")] + public int bufferSize; + + [JsonProperty("log_level")] + public string logLevel; + + [JsonProperty("log_format")] + public string logFormat; + + [JsonProperty("max_retries")] + public int maxRetries; + + [JsonProperty("retry_delay")] + public float retryDelay; + } +} diff --git a/MCPForUnity/Editor/Models/ServerConfig.cs.meta b/MCPForUnity/Editor/Models/ServerConfig.cs.meta new file mode 100644 index 00000000..6e675e9e --- /dev/null +++ b/MCPForUnity/Editor/Models/ServerConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e4e45386fcc282249907c2e3c7e5d9c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Setup.meta b/MCPForUnity/Editor/Setup.meta new file mode 100644 index 00000000..1157b1e9 --- /dev/null +++ b/MCPForUnity/Editor/Setup.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 600c9cb20c329d761bfa799158a87bac +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Setup/SetupWizard.cs b/MCPForUnity/Editor/Setup/SetupWizard.cs new file mode 100644 index 00000000..a97926ea --- /dev/null +++ b/MCPForUnity/Editor/Setup/SetupWizard.cs @@ -0,0 +1,150 @@ +using System; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Windows; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Setup +{ + /// + /// Handles automatic triggering of the setup wizard + /// + [InitializeOnLoad] + public static class SetupWizard + { + private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted"; + private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed"; + private static bool _hasCheckedThisSession = false; + + static SetupWizard() + { + // Skip in batch mode + if (Application.isBatchMode) + return; + + // Show setup wizard on package import + EditorApplication.delayCall += CheckSetupNeeded; + } + + /// + /// Check if setup wizard should be shown + /// + private static void CheckSetupNeeded() + { + if (_hasCheckedThisSession) + return; + + _hasCheckedThisSession = true; + + try + { + // Check if setup was already completed or dismissed in previous sessions + bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false); + bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false); + + // Only show setup wizard if it hasn't been completed or dismissed before + if (!(setupCompleted || setupDismissed)) + { + McpLog.Info("Package imported - showing setup wizard", always: false); + + var dependencyResult = DependencyManager.CheckAllDependencies(); + EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult); + } + else + { + McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false); + } + } + catch (Exception ex) + { + McpLog.Error($"Error checking setup status: {ex.Message}"); + } + } + + /// + /// Show the setup wizard window + /// + public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null) + { + try + { + dependencyResult ??= DependencyManager.CheckAllDependencies(); + SetupWizardWindow.ShowWindow(dependencyResult); + } + catch (Exception ex) + { + McpLog.Error($"Error showing setup wizard: {ex.Message}"); + } + } + + /// + /// Mark setup as completed + /// + public static void MarkSetupCompleted() + { + EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true); + McpLog.Info("Setup marked as completed"); + } + + /// + /// Mark setup as dismissed + /// + public static void MarkSetupDismissed() + { + EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true); + McpLog.Info("Setup marked as dismissed"); + } + + /// + /// Force show setup wizard (for manual invocation) + /// + [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)] + public static void ShowSetupWizardManual() + { + ShowSetupWizard(); + } + + /// + /// Check dependencies and show status + /// + [MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)] + public static void CheckDependencies() + { + var result = DependencyManager.CheckAllDependencies(); + + if (!result.IsSystemReady) + { + bool showWizard = EditorUtility.DisplayDialog( + "MCP for Unity - Dependencies", + $"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?", + "Open Setup Wizard", + "Close" + ); + + if (showWizard) + { + ShowSetupWizard(result); + } + } + else + { + EditorUtility.DisplayDialog( + "MCP for Unity - Dependencies", + "✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.", + "OK" + ); + } + } + + /// + /// Open MCP Client Configuration window + /// + [MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)] + public static void OpenClientConfiguration() + { + Windows.MCPForUnityEditorWindow.ShowWindow(); + } + } +} diff --git a/MCPForUnity/Editor/Setup/SetupWizard.cs.meta b/MCPForUnity/Editor/Setup/SetupWizard.cs.meta new file mode 100644 index 00000000..1a0e4e5f --- /dev/null +++ b/MCPForUnity/Editor/Setup/SetupWizard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 345678901234abcdef0123456789abcd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Setup/SetupWizardWindow.cs b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs new file mode 100644 index 00000000..7229be97 --- /dev/null +++ b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs @@ -0,0 +1,726 @@ +using System; +using System.Linq; +using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Setup +{ + /// + /// Setup wizard window for guiding users through dependency installation + /// + public class SetupWizardWindow : EditorWindow + { + private DependencyCheckResult _dependencyResult; + private Vector2 _scrollPosition; + private int _currentStep = 0; + private McpClients _mcpClients; + private int _selectedClientIndex = 0; + + private readonly string[] _stepTitles = { + "Setup", + "Configure", + "Complete" + }; + + public static void ShowWindow(DependencyCheckResult dependencyResult = null) + { + var window = GetWindow("MCP for Unity Setup"); + window.minSize = new Vector2(500, 400); + window.maxSize = new Vector2(800, 600); + window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies(); + window.Show(); + } + + private void OnEnable() + { + if (_dependencyResult == null) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + + _mcpClients = new McpClients(); + + // Check client configurations on startup + foreach (var client in _mcpClients.clients) + { + CheckClientConfiguration(client); + } + } + + private void OnGUI() + { + DrawHeader(); + DrawProgressBar(); + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + + switch (_currentStep) + { + case 0: DrawSetupStep(); break; + case 1: DrawConfigureStep(); break; + case 2: DrawCompleteStep(); break; + } + + EditorGUILayout.EndScrollView(); + + DrawFooter(); + } + + private void DrawHeader() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}"); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // Step title + var titleStyle = new GUIStyle(EditorStyles.largeLabel) + { + fontSize = 16, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle); + EditorGUILayout.Space(); + } + + private void DrawProgressBar() + { + var rect = EditorGUILayout.GetControlRect(false, 4); + var progress = (_currentStep + 1) / (float)_stepTitles.Length; + EditorGUI.ProgressBar(rect, progress, ""); + EditorGUILayout.Space(); + } + + private void DrawSetupStep() + { + // Welcome section + DrawSectionTitle("MCP for Unity Setup"); + + EditorGUILayout.LabelField( + "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + // Dependency check section + EditorGUILayout.BeginHorizontal(); + DrawSectionTitle("System Check", 14); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20))) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + EditorGUILayout.EndHorizontal(); + + // Show simplified dependency status + foreach (var dep in _dependencyResult.Dependencies) + { + DrawSimpleDependencyStatus(dep); + } + + // Overall status and installation guidance + EditorGUILayout.Space(); + if (!_dependencyResult.IsSystemReady) + { + // Only show critical warnings when dependencies are actually missing + EditorGUILayout.HelpBox( + "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.", + MessageType.Warning + ); + + EditorGUILayout.Space(); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + DrawErrorStatus("Installation Required"); + + var recommendations = DependencyManager.GetInstallationRecommendations(); + EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel); + + EditorGUILayout.Space(); + if (GUILayout.Button("Open Installation Links", GUILayout.Height(25))) + { + OpenInstallationUrls(); + } + EditorGUILayout.EndVertical(); + } + else + { + DrawSuccessStatus("System Ready"); + EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel); + } + } + + + + private void DrawCompleteStep() + { + DrawSectionTitle("Setup Complete"); + + // Refresh dependency check with caching to avoid heavy operations on every repaint + if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + + if (_dependencyResult.IsSystemReady) + { + DrawSuccessStatus("MCP for Unity Ready!"); + + EditorGUILayout.HelpBox( + "🎉 MCP for Unity is now set up and ready to use!\n\n" + + "• Dependencies verified\n" + + "• MCP server ready\n" + + "• Client configuration accessible", + MessageType.Info + ); + + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Documentation", GUILayout.Height(30))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp"); + } + if (GUILayout.Button("Client Settings", GUILayout.Height(30))) + { + Windows.MCPForUnityEditorWindow.ShowWindow(); + } + EditorGUILayout.EndHorizontal(); + } + else + { + DrawErrorStatus("Setup Incomplete - Package Non-Functional"); + + EditorGUILayout.HelpBox( + "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" + + "Install ALL required dependencies before the package will function.", + MessageType.Error + ); + + var missingDeps = _dependencyResult.GetMissingRequired(); + if (missingDeps.Count > 0) + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel); + foreach (var dep in missingDeps) + { + EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label); + } + } + + EditorGUILayout.Space(); + if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) + { + _currentStep = 0; + } + } + } + + // Helper methods for consistent UI components + private void DrawSectionTitle(string title, int fontSize = 16) + { + var titleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = fontSize, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(title, titleStyle); + EditorGUILayout.Space(); + } + + private void DrawSuccessStatus(string message) + { + var originalColor = GUI.color; + GUI.color = Color.green; + EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel); + GUI.color = originalColor; + EditorGUILayout.Space(); + } + + private void DrawErrorStatus(string message) + { + var originalColor = GUI.color; + GUI.color = Color.red; + EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel); + GUI.color = originalColor; + EditorGUILayout.Space(); + } + + private void DrawSimpleDependencyStatus(DependencyStatus dep) + { + EditorGUILayout.BeginHorizontal(); + + var statusIcon = dep.IsAvailable ? "✓" : "✗"; + var statusColor = dep.IsAvailable ? Color.green : Color.red; + + var originalColor = GUI.color; + GUI.color = statusColor; + GUILayout.Label(statusIcon, GUILayout.Width(20)); + EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel); + GUI.color = originalColor; + + if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage)) + { + EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel); + } + + EditorGUILayout.EndHorizontal(); + } + + private void DrawConfigureStep() + { + DrawSectionTitle("AI Client Configuration"); + + // Check dependencies first (with caching to avoid heavy operations on every repaint) + if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + if (!_dependencyResult.IsSystemReady) + { + DrawErrorStatus("Cannot Configure - System Requirements Not Met"); + + EditorGUILayout.HelpBox( + "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.", + MessageType.Warning + ); + + if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) + { + _currentStep = 0; + } + return; + } + + EditorGUILayout.LabelField( + "Configure your AI assistants to work with Unity. Select a client below to set it up:", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + // Client selection and configuration + if (_mcpClients.clients.Count > 0) + { + // Client selector dropdown + string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray(); + EditorGUI.BeginChangeCheck(); + _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames); + if (EditorGUI.EndChangeCheck()) + { + _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1); + // Refresh client status when selection changes + CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]); + } + + EditorGUILayout.Space(); + + var selectedClient = _mcpClients.clients[_selectedClientIndex]; + DrawClientConfigurationInWizard(selectedClient); + + EditorGUILayout.Space(); + + // Batch configuration option + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel); + EditorGUILayout.LabelField( + "Automatically configure all detected AI clients at once:", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30))) + { + ConfigureAllClientsInWizard(); + } + EditorGUILayout.EndVertical(); + } + else + { + EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info); + } + + EditorGUILayout.Space(); + EditorGUILayout.HelpBox( + "💡 You might need to restart your AI client after configuring.", + MessageType.Info + ); + } + + private void DrawFooter() + { + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + + // Back button + GUI.enabled = _currentStep > 0; + if (GUILayout.Button("Back", GUILayout.Width(60))) + { + _currentStep--; + } + + GUILayout.FlexibleSpace(); + + // Skip button + if (GUILayout.Button("Skip", GUILayout.Width(60))) + { + bool dismiss = EditorUtility.DisplayDialog( + "Skip Setup", + "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" + + "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)", + "Skip Anyway", + "Cancel" + ); + + if (dismiss) + { + SetupWizard.MarkSetupDismissed(); + Close(); + } + } + + // Next/Done button + GUI.enabled = true; + string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next"; + + if (GUILayout.Button(buttonText, GUILayout.Width(80))) + { + if (_currentStep == _stepTitles.Length - 1) + { + SetupWizard.MarkSetupCompleted(); + Close(); + } + else + { + _currentStep++; + } + } + + GUI.enabled = true; + EditorGUILayout.EndHorizontal(); + } + + private void DrawClientConfigurationInWizard(McpClient client) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // Show current status + var statusColor = GetClientStatusColor(client); + var originalColor = GUI.color; + GUI.color = statusColor; + EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label); + GUI.color = originalColor; + + EditorGUILayout.Space(); + + // Configuration buttons + EditorGUILayout.BeginHorizontal(); + + if (client.mcpType == McpTypes.ClaudeCode) + { + // Special handling for Claude Code + bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); + if (claudeAvailable) + { + bool isConfigured = client.status == McpStatus.Configured; + string buttonText = isConfigured ? "Unregister" : "Register"; + if (GUILayout.Button($"{buttonText} with Claude Code")) + { + if (isConfigured) + { + UnregisterFromClaudeCode(client); + } + else + { + RegisterWithClaudeCode(client); + } + } + } + else + { + EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning); + if (GUILayout.Button("Open Claude Code Website")) + { + Application.OpenURL("https://claude.ai/download"); + } + } + } + else + { + // Standard client configuration + if (GUILayout.Button($"Configure {client.name}")) + { + ConfigureClientInWizard(client); + } + + if (GUILayout.Button("Manual Setup")) + { + ShowManualSetupInWizard(client); + } + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + + private Color GetClientStatusColor(McpClient client) + { + return client.status switch + { + McpStatus.Configured => Color.green, + McpStatus.Running => Color.green, + McpStatus.Connected => Color.green, + McpStatus.IncorrectPath => Color.yellow, + McpStatus.CommunicationError => Color.yellow, + McpStatus.NoResponse => Color.yellow, + _ => Color.red + }; + } + + private void ConfigureClientInWizard(McpClient client) + { + try + { + string result = PerformClientConfiguration(client); + + EditorUtility.DisplayDialog( + $"{client.name} Configuration", + result, + "OK" + ); + + // Refresh client status + CheckClientConfiguration(client); + Repaint(); + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog( + "Configuration Error", + $"Failed to configure {client.name}: {ex.Message}", + "OK" + ); + } + } + + private void ConfigureAllClientsInWizard() + { + int successCount = 0; + int totalCount = _mcpClients.clients.Count; + + foreach (var client in _mcpClients.clients) + { + try + { + if (client.mcpType == McpTypes.ClaudeCode) + { + if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured) + { + RegisterWithClaudeCode(client); + successCount++; + } + else if (client.status == McpStatus.Configured) + { + successCount++; // Already configured + } + } + else + { + string result = PerformClientConfiguration(client); + if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase)) + { + successCount++; + } + } + + CheckClientConfiguration(client); + } + catch (System.Exception ex) + { + McpLog.Error($"Failed to configure {client.name}: {ex.Message}"); + } + } + + EditorUtility.DisplayDialog( + "Batch Configuration Complete", + $"Successfully configured {successCount} out of {totalCount} clients.\n\n" + + "Restart your AI clients for changes to take effect.", + "OK" + ); + + Repaint(); + } + + private void RegisterWithClaudeCode(McpClient client) + { + try + { + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + string claudePath = ExecPath.ResolveClaude(); + string uvPath = ExecPath.ResolveUv() ?? "uv"; + + string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; + + if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend())) + { + if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase)) + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK"); + } + else + { + throw new System.Exception($"Registration failed: {stderr}"); + } + } + else + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK"); + } + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK"); + } + } + + private void UnregisterFromClaudeCode(McpClient client) + { + try + { + string claudePath = ExecPath.ResolveClaude(); + if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend())) + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK"); + } + else + { + throw new System.Exception($"Unregistration failed: {stderr}"); + } + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK"); + } + } + + private string PerformClientConfiguration(McpClient client) + { + // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + + if (string.IsNullOrEmpty(pythonDir)) + { + return "Manual configuration required - Python server directory not found."; + } + + McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); + return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); + } + + private void ShowManualSetupInWizard(McpClient client) + { + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + string uvPath = ServerInstaller.FindUvPath(); + + if (string.IsNullOrEmpty(uvPath)) + { + EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK"); + return; + } + + // Build manual configuration using the sophisticated helper logic + string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); + string manualConfig; + + if (result == "Configured successfully") + { + // Read back the configuration that was written + try + { + manualConfig = System.IO.File.ReadAllText(configPath); + } + catch + { + manualConfig = "Configuration written successfully, but could not read back for display."; + } + } + else + { + manualConfig = $"Configuration failed: {result}"; + } + + EditorUtility.DisplayDialog( + $"Manual Setup - {client.name}", + $"Configuration file location:\n{configPath}\n\n" + + $"Configuration result:\n{manualConfig}", + "OK" + ); + } + + private void CheckClientConfiguration(McpClient client) + { + // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic + try + { + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + if (System.IO.File.Exists(configPath)) + { + client.configStatus = "Configured"; + client.status = McpStatus.Configured; + } + else + { + client.configStatus = "Not Configured"; + client.status = McpStatus.NotConfigured; + } + } + catch + { + client.configStatus = "Error"; + client.status = McpStatus.Error; + } + } + + private void OpenInstallationUrls() + { + var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls(); + + bool openPython = EditorUtility.DisplayDialog( + "Open Installation URLs", + "Open Python installation page?", + "Yes", + "No" + ); + + if (openPython) + { + Application.OpenURL(pythonUrl); + } + + bool openUV = EditorUtility.DisplayDialog( + "Open Installation URLs", + "Open UV installation page?", + "Yes", + "No" + ); + + if (openUV) + { + Application.OpenURL(uvUrl); + } + } + } +} diff --git a/MCPForUnity/Editor/Setup/SetupWizardWindow.cs.meta b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs.meta new file mode 100644 index 00000000..5361de3d --- /dev/null +++ b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45678901234abcdef0123456789abcde +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/MCPForUnity/Editor/Tools.meta b/MCPForUnity/Editor/Tools.meta new file mode 100644 index 00000000..2bc55f08 --- /dev/null +++ b/MCPForUnity/Editor/Tools.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c97b83a6ac92a704b864eef27c3d285b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/CommandRegistry.cs b/MCPForUnity/Editor/Tools/CommandRegistry.cs new file mode 100644 index 00000000..79003d55 --- /dev/null +++ b/MCPForUnity/Editor/Tools/CommandRegistry.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Registry for all MCP command handlers via reflection. + /// + public static class CommandRegistry + { + private static readonly Dictionary> _handlers = new(); + private static bool _initialized = false; + + /// + /// Initialize and auto-discover all tools marked with [McpForUnityTool] + /// + public static void Initialize() + { + if (_initialized) return; + + AutoDiscoverTools(); + _initialized = true; + } + + /// + /// Convert PascalCase or camelCase to snake_case + /// + private static string ToSnakeCase(string name) + { + if (string.IsNullOrEmpty(name)) return name; + + // Insert underscore before uppercase letters (except first) + var s1 = Regex.Replace(name, "(.)([A-Z][a-z]+)", "$1_$2"); + var s2 = Regex.Replace(s1, "([a-z0-9])([A-Z])", "$1_$2"); + return s2.ToLower(); + } + + /// + /// Auto-discover all types with [McpForUnityTool] attribute + /// + private static void AutoDiscoverTools() + { + try + { + var toolTypes = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) + .SelectMany(a => + { + try { return a.GetTypes(); } + catch { return new Type[0]; } + }) + .Where(t => t.GetCustomAttribute() != null); + + foreach (var type in toolTypes) + { + RegisterToolType(type); + } + + McpLog.Info($"Auto-discovered {_handlers.Count} tools"); + } + catch (Exception ex) + { + McpLog.Error($"Failed to auto-discover MCP tools: {ex.Message}"); + } + } + + private static void RegisterToolType(Type type) + { + var attr = type.GetCustomAttribute(); + + // Get command name (explicit or auto-generated) + string commandName = attr.CommandName; + if (string.IsNullOrEmpty(commandName)) + { + commandName = ToSnakeCase(type.Name); + } + + // Check for duplicate command names + if (_handlers.ContainsKey(commandName)) + { + McpLog.Warn( + $"Duplicate command name '{commandName}' detected. " + + $"Tool {type.Name} will override previously registered handler." + ); + } + + // Find HandleCommand method + var method = type.GetMethod( + "HandleCommand", + BindingFlags.Public | BindingFlags.Static, + null, + new[] { typeof(JObject) }, + null + ); + + if (method == null) + { + McpLog.Warn( + $"MCP tool {type.Name} is marked with [McpForUnityTool] " + + $"but has no public static HandleCommand(JObject) method" + ); + return; + } + + try + { + var handler = (Func)Delegate.CreateDelegate( + typeof(Func), + method + ); + _handlers[commandName] = handler; + } + catch (Exception ex) + { + McpLog.Error($"Failed to register tool {type.Name}: {ex.Message}"); + } + } + + /// + /// Get a command handler by name + /// + public static Func GetHandler(string commandName) + { + if (!_handlers.TryGetValue(commandName, out var handler)) + { + throw new InvalidOperationException( + $"Unknown or unsupported command type: {commandName}" + ); + } + return handler; + } + } +} diff --git a/MCPForUnity/Editor/Tools/CommandRegistry.cs.meta b/MCPForUnity/Editor/Tools/CommandRegistry.cs.meta new file mode 100644 index 00000000..15ec884b --- /dev/null +++ b/MCPForUnity/Editor/Tools/CommandRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b61b5a84813b5749a5c64422694a0fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/ManageAsset.cs b/MCPForUnity/Editor/Tools/ManageAsset.cs new file mode 100644 index 00000000..1a952f37 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageAsset.cs @@ -0,0 +1,1331 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Helpers; // For Response class +using static MCPForUnity.Editor.Tools.ManageGameObject; + +#if UNITY_6000_0_OR_NEWER +using PhysicsMaterialType = UnityEngine.PhysicsMaterial; +using PhysicsMaterialCombine = UnityEngine.PhysicsMaterialCombine; +#else +using PhysicsMaterialType = UnityEngine.PhysicMaterial; +using PhysicsMaterialCombine = UnityEngine.PhysicMaterialCombine; +#endif + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Handles asset management operations within the Unity project. + /// + [McpForUnityTool("manage_asset")] + public static class ManageAsset + { + // --- Main Handler --- + + // Define the list of valid actions + private static readonly List ValidActions = new List + { + "import", + "create", + "modify", + "delete", + "duplicate", + "move", + "rename", + "search", + "get_info", + "create_folder", + "get_components", + }; + + public static object HandleCommand(JObject @params) + { + string action = @params["action"]?.ToString().ToLower(); + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + + // Check if the action is valid before switching + if (!ValidActions.Contains(action)) + { + string validActionsList = string.Join(", ", ValidActions); + return Response.Error( + $"Unknown action: '{action}'. Valid actions are: {validActionsList}" + ); + } + + // Common parameters + string path = @params["path"]?.ToString(); + + try + { + switch (action) + { + case "import": + // Note: Unity typically auto-imports. This might re-import or configure import settings. + return ReimportAsset(path, @params["properties"] as JObject); + case "create": + return CreateAsset(@params); + case "modify": + return ModifyAsset(path, @params["properties"] as JObject); + case "delete": + return DeleteAsset(path); + case "duplicate": + return DuplicateAsset(path, @params["destination"]?.ToString()); + case "move": // Often same as rename if within Assets/ + case "rename": + return MoveOrRenameAsset(path, @params["destination"]?.ToString()); + case "search": + return SearchAssets(@params); + case "get_info": + return GetAssetInfo( + path, + @params["generatePreview"]?.ToObject() ?? false + ); + case "create_folder": // Added specific action for clarity + return CreateFolder(path); + case "get_components": + return GetComponentsFromAsset(path); + + default: + // This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications. + string validActionsListDefault = string.Join(", ", ValidActions); + return Response.Error( + $"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}" + ); + } + } + catch (Exception e) + { + Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}"); + return Response.Error( + $"Internal error processing action '{action}' on '{path}': {e.Message}" + ); + } + } + + // --- Action Implementations --- + + private static object ReimportAsset(string path, JObject properties) + { + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for reimport."); + string fullPath = AssetPathUtility.SanitizeAssetPath(path); + if (!AssetExists(fullPath)) + return Response.Error($"Asset not found at path: {fullPath}"); + + try + { + // TODO: Apply importer properties before reimporting? + // This is complex as it requires getting the AssetImporter, casting it, + // applying properties via reflection or specific methods, saving, then reimporting. + if (properties != null && properties.HasValues) + { + Debug.LogWarning( + "[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet." + ); + // AssetImporter importer = AssetImporter.GetAtPath(fullPath); + // if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); } + } + + AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); + // AssetDatabase.Refresh(); // Usually ImportAsset handles refresh + return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath)); + } + catch (Exception e) + { + return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}"); + } + } + + private static object CreateAsset(JObject @params) + { + string path = @params["path"]?.ToString(); + string assetType = @params["assetType"]?.ToString(); + JObject properties = @params["properties"] as JObject; + + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for create."); + if (string.IsNullOrEmpty(assetType)) + return Response.Error("'assetType' is required for create."); + + string fullPath = AssetPathUtility.SanitizeAssetPath(path); + string directory = Path.GetDirectoryName(fullPath); + + // Ensure directory exists + if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory))) + { + Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory)); + AssetDatabase.Refresh(); // Make sure Unity knows about the new folder + } + + if (AssetExists(fullPath)) + return Response.Error($"Asset already exists at path: {fullPath}"); + + try + { + UnityEngine.Object newAsset = null; + string lowerAssetType = assetType.ToLowerInvariant(); + + // Handle common asset types + if (lowerAssetType == "folder") + { + return CreateFolder(path); // Use dedicated method + } + else if (lowerAssetType == "material") + { + // Prefer provided shader; fall back to common pipelines + var requested = properties?["shader"]?.ToString(); + Shader shader = + (!string.IsNullOrEmpty(requested) ? Shader.Find(requested) : null) + ?? Shader.Find("Universal Render Pipeline/Lit") + ?? Shader.Find("HDRP/Lit") + ?? Shader.Find("Standard") + ?? Shader.Find("Unlit/Color"); + if (shader == null) + return Response.Error($"Could not find a suitable shader (requested: '{requested ?? "none"}')."); + + var mat = new Material(shader); + if (properties != null) + ApplyMaterialProperties(mat, properties); + AssetDatabase.CreateAsset(mat, fullPath); + newAsset = mat; + } + else if (lowerAssetType == "physicsmaterial") + { + PhysicsMaterialType pmat = new PhysicsMaterialType(); + if (properties != null) + ApplyPhysicsMaterialProperties(pmat, properties); + AssetDatabase.CreateAsset(pmat, fullPath); + newAsset = pmat; + } + else if (lowerAssetType == "scriptableobject") + { + string scriptClassName = properties?["scriptClass"]?.ToString(); + if (string.IsNullOrEmpty(scriptClassName)) + return Response.Error( + "'scriptClass' property required when creating ScriptableObject asset." + ); + + Type scriptType = ComponentResolver.TryResolve(scriptClassName, out var resolvedType, out var error) ? resolvedType : null; + if ( + scriptType == null + || !typeof(ScriptableObject).IsAssignableFrom(scriptType) + ) + { + var reason = scriptType == null + ? (string.IsNullOrEmpty(error) ? "Type not found." : error) + : "Type found but does not inherit from ScriptableObject."; + return Response.Error($"Script class '{scriptClassName}' invalid: {reason}"); + } + + ScriptableObject so = ScriptableObject.CreateInstance(scriptType); + // TODO: Apply properties from JObject to the ScriptableObject instance? + AssetDatabase.CreateAsset(so, fullPath); + newAsset = so; + } + else if (lowerAssetType == "prefab") + { + // Creating prefabs usually involves saving an existing GameObject hierarchy. + // A common pattern is to create an empty GameObject, configure it, and then save it. + return Response.Error( + "Creating prefabs programmatically usually requires a source GameObject. Use manage_gameobject to create/configure, then save as prefab via a separate mechanism or future enhancement." + ); + // Example (conceptual): + // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString()); + // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath); + } + // TODO: Add more asset types (Animation Controller, Scene, etc.) + else + { + // Generic creation attempt (might fail or create empty files) + // For some types, just creating the file might be enough if Unity imports it. + // File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close(); + // AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it + // newAsset = AssetDatabase.LoadAssetAtPath(fullPath); + return Response.Error( + $"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject." + ); + } + + if ( + newAsset == null + && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath)) + ) // Check if it wasn't a folder and asset wasn't created + { + return Response.Error( + $"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details." + ); + } + + AssetDatabase.SaveAssets(); + // AssetDatabase.Refresh(); // CreateAsset often handles refresh + return Response.Success( + $"Asset '{fullPath}' created successfully.", + GetAssetData(fullPath) + ); + } + catch (Exception e) + { + return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}"); + } + } + + private static object CreateFolder(string path) + { + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for create_folder."); + string fullPath = AssetPathUtility.SanitizeAssetPath(path); + string parentDir = Path.GetDirectoryName(fullPath); + string folderName = Path.GetFileName(fullPath); + + if (AssetExists(fullPath)) + { + // Check if it's actually a folder already + if (AssetDatabase.IsValidFolder(fullPath)) + { + return Response.Success( + $"Folder already exists at path: {fullPath}", + GetAssetData(fullPath) + ); + } + else + { + return Response.Error( + $"An asset (not a folder) already exists at path: {fullPath}" + ); + } + } + + try + { + // Ensure parent exists + if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir)) + { + // Recursively create parent folders if needed (AssetDatabase handles this internally) + // Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh(); + } + + string guid = AssetDatabase.CreateFolder(parentDir, folderName); + if (string.IsNullOrEmpty(guid)) + { + return Response.Error( + $"Failed to create folder '{fullPath}'. Check logs and permissions." + ); + } + + // AssetDatabase.Refresh(); // CreateFolder usually handles refresh + return Response.Success( + $"Folder '{fullPath}' created successfully.", + GetAssetData(fullPath) + ); + } + catch (Exception e) + { + return Response.Error($"Failed to create folder '{fullPath}': {e.Message}"); + } + } + + private static object ModifyAsset(string path, JObject properties) + { + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for modify."); + if (properties == null || !properties.HasValues) + return Response.Error("'properties' are required for modify."); + + string fullPath = AssetPathUtility.SanitizeAssetPath(path); + if (!AssetExists(fullPath)) + return Response.Error($"Asset not found at path: {fullPath}"); + + try + { + UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath( + fullPath + ); + if (asset == null) + return Response.Error($"Failed to load asset at path: {fullPath}"); + + bool modified = false; // Flag to track if any changes were made + + // --- NEW: Handle GameObject / Prefab Component Modification --- + if (asset is GameObject gameObject) + { + // Iterate through the properties JSON: keys are component names, values are properties objects for that component + foreach (var prop in properties.Properties()) + { + string componentName = prop.Name; // e.g., "Collectible" + // Check if the value associated with the component name is actually an object containing properties + if ( + prop.Value is JObject componentProperties + && componentProperties.HasValues + ) // e.g., {"bobSpeed": 2.0} + { + // Resolve component type via ComponentResolver, then fetch by Type + Component targetComponent = null; + bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError); + if (resolved) + { + targetComponent = gameObject.GetComponent(compType); + } + + // Only warn about resolution failure if component also not found + if (targetComponent == null && !resolved) + { + Debug.LogWarning( + $"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}" + ); + } + + if (targetComponent != null) + { + // Apply the nested properties (e.g., bobSpeed) to the found component instance + // Use |= to ensure 'modified' becomes true if any component is successfully modified + modified |= ApplyObjectProperties( + targetComponent, + componentProperties + ); + } + else + { + // Log a warning if a specified component couldn't be found + Debug.LogWarning( + $"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component." + ); + } + } + else + { + // Log a warning if the structure isn't {"ComponentName": {"prop": value}} + // We could potentially try to apply this property directly to the GameObject here if needed, + // but the primary goal is component modification. + Debug.LogWarning( + $"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping." + ); + } + } + // Note: 'modified' is now true if ANY component property was successfully changed. + } + // --- End NEW --- + + // --- Existing logic for other asset types (now as else-if) --- + // Example: Modifying a Material + else if (asset is Material material) + { + // Apply properties directly to the material. If this modifies, it sets modified=true. + // Use |= in case the asset was already marked modified by previous logic (though unlikely here) + modified |= ApplyMaterialProperties(material, properties); + } + // Example: Modifying a ScriptableObject + else if (asset is ScriptableObject so) + { + // Apply properties directly to the ScriptableObject. + modified |= ApplyObjectProperties(so, properties); // General helper + } + // Example: Modifying TextureImporter settings + else if (asset is Texture) + { + AssetImporter importer = AssetImporter.GetAtPath(fullPath); + if (importer is TextureImporter textureImporter) + { + bool importerModified = ApplyObjectProperties(textureImporter, properties); + if (importerModified) + { + // Importer settings need saving and reimporting + AssetDatabase.WriteImportSettingsIfDirty(fullPath); + AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes + modified = true; // Mark overall operation as modified + } + } + else + { + Debug.LogWarning($"Could not get TextureImporter for {fullPath}."); + } + } + // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.) + else // Fallback for other asset types OR direct properties on non-GameObject assets + { + // This block handles non-GameObject/Material/ScriptableObject/Texture assets. + // Attempts to apply properties directly to the asset itself. + Debug.LogWarning( + $"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself." + ); + modified |= ApplyObjectProperties(asset, properties); + } + // --- End Existing Logic --- + + // Check if any modification happened (either component or direct asset modification) + if (modified) + { + // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it. + EditorUtility.SetDirty(asset); + // Save all modified assets to disk. + AssetDatabase.SaveAssets(); + // Refresh might be needed in some edge cases, but SaveAssets usually covers it. + // AssetDatabase.Refresh(); + return Response.Success( + $"Asset '{fullPath}' modified successfully.", + GetAssetData(fullPath) + ); + } + else + { + // If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed. + return Response.Success( + $"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.", + GetAssetData(fullPath) + ); + // Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath)); + } + } + catch (Exception e) + { + // Log the detailed error internally + Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}"); + // Return a user-friendly error message + return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}"); + } + } + + private static object DeleteAsset(string path) + { + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for delete."); + string fullPath = AssetPathUtility.SanitizeAssetPath(path); + if (!AssetExists(fullPath)) + return Response.Error($"Asset not found at path: {fullPath}"); + + try + { + bool success = AssetDatabase.DeleteAsset(fullPath); + if (success) + { + // AssetDatabase.Refresh(); // DeleteAsset usually handles refresh + return Response.Success($"Asset '{fullPath}' deleted successfully."); + } + else + { + // This might happen if the file couldn't be deleted (e.g., locked) + return Response.Error( + $"Failed to delete asset '{fullPath}'. Check logs or if the file is locked." + ); + } + } + catch (Exception e) + { + return Response.Error($"Error deleting asset '{fullPath}': {e.Message}"); + } + } + + private static object DuplicateAsset(string path, string destinationPath) + { + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for duplicate."); + + string sourcePath = AssetPathUtility.SanitizeAssetPath(path); + if (!AssetExists(sourcePath)) + return Response.Error($"Source asset not found at path: {sourcePath}"); + + string destPath; + if (string.IsNullOrEmpty(destinationPath)) + { + // Generate a unique path if destination is not provided + destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath); + } + else + { + destPath = AssetPathUtility.SanitizeAssetPath(destinationPath); + if (AssetExists(destPath)) + return Response.Error($"Asset already exists at destination path: {destPath}"); + // Ensure destination directory exists + EnsureDirectoryExists(Path.GetDirectoryName(destPath)); + } + + try + { + bool success = AssetDatabase.CopyAsset(sourcePath, destPath); + if (success) + { + // AssetDatabase.Refresh(); + return Response.Success( + $"Asset '{sourcePath}' duplicated to '{destPath}'.", + GetAssetData(destPath) + ); + } + else + { + return Response.Error( + $"Failed to duplicate asset from '{sourcePath}' to '{destPath}'." + ); + } + } + catch (Exception e) + { + return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}"); + } + } + + private static object MoveOrRenameAsset(string path, string destinationPath) + { + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for move/rename."); + if (string.IsNullOrEmpty(destinationPath)) + return Response.Error("'destination' path is required for move/rename."); + + string sourcePath = AssetPathUtility.SanitizeAssetPath(path); + string destPath = AssetPathUtility.SanitizeAssetPath(destinationPath); + + if (!AssetExists(sourcePath)) + return Response.Error($"Source asset not found at path: {sourcePath}"); + if (AssetExists(destPath)) + return Response.Error( + $"An asset already exists at the destination path: {destPath}" + ); + + // Ensure destination directory exists + EnsureDirectoryExists(Path.GetDirectoryName(destPath)); + + try + { + // Validate will return an error string if failed, null if successful + string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath); + if (!string.IsNullOrEmpty(error)) + { + return Response.Error( + $"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}" + ); + } + + string guid = AssetDatabase.MoveAsset(sourcePath, destPath); + if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success + { + // AssetDatabase.Refresh(); // MoveAsset usually handles refresh + return Response.Success( + $"Asset moved/renamed from '{sourcePath}' to '{destPath}'.", + GetAssetData(destPath) + ); + } + else + { + // This case might not be reachable if ValidateMoveAsset passes, but good to have + return Response.Error( + $"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'." + ); + } + } + catch (Exception e) + { + return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}"); + } + } + + private static object SearchAssets(JObject @params) + { + string searchPattern = @params["searchPattern"]?.ToString(); + string filterType = @params["filterType"]?.ToString(); + string pathScope = @params["path"]?.ToString(); // Use path as folder scope + string filterDateAfterStr = @params["filterDateAfter"]?.ToString(); + int pageSize = @params["pageSize"]?.ToObject() ?? 50; // Default page size + int pageNumber = @params["pageNumber"]?.ToObject() ?? 1; // Default page number (1-based) + bool generatePreview = @params["generatePreview"]?.ToObject() ?? false; + + List searchFilters = new List(); + if (!string.IsNullOrEmpty(searchPattern)) + searchFilters.Add(searchPattern); + if (!string.IsNullOrEmpty(filterType)) + searchFilters.Add($"t:{filterType}"); + + string[] folderScope = null; + if (!string.IsNullOrEmpty(pathScope)) + { + folderScope = new string[] { AssetPathUtility.SanitizeAssetPath(pathScope) }; + if (!AssetDatabase.IsValidFolder(folderScope[0])) + { + // Maybe the user provided a file path instead of a folder? + // We could search in the containing folder, or return an error. + Debug.LogWarning( + $"Search path '{folderScope[0]}' is not a valid folder. Searching entire project." + ); + folderScope = null; // Search everywhere if path isn't a folder + } + } + + DateTime? filterDateAfter = null; + if (!string.IsNullOrEmpty(filterDateAfterStr)) + { + if ( + DateTime.TryParse( + filterDateAfterStr, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out DateTime parsedDate + ) + ) + { + filterDateAfter = parsedDate; + } + else + { + Debug.LogWarning( + $"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format." + ); + } + } + + try + { + string[] guids = AssetDatabase.FindAssets( + string.Join(" ", searchFilters), + folderScope + ); + List results = new List(); + int totalFound = 0; + + foreach (string guid in guids) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrEmpty(assetPath)) + continue; + + // Apply date filter if present + if (filterDateAfter.HasValue) + { + DateTime lastWriteTime = File.GetLastWriteTimeUtc( + Path.Combine(Directory.GetCurrentDirectory(), assetPath) + ); + if (lastWriteTime <= filterDateAfter.Value) + { + continue; // Skip assets older than or equal to the filter date + } + } + + totalFound++; // Count matching assets before pagination + results.Add(GetAssetData(assetPath, generatePreview)); + } + + // Apply pagination + int startIndex = (pageNumber - 1) * pageSize; + var pagedResults = results.Skip(startIndex).Take(pageSize).ToList(); + + return Response.Success( + $"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).", + new + { + totalAssets = totalFound, + pageSize = pageSize, + pageNumber = pageNumber, + assets = pagedResults, + } + ); + } + catch (Exception e) + { + return Response.Error($"Error searching assets: {e.Message}"); + } + } + + private static object GetAssetInfo(string path, bool generatePreview) + { + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for get_info."); + string fullPath = AssetPathUtility.SanitizeAssetPath(path); + if (!AssetExists(fullPath)) + return Response.Error($"Asset not found at path: {fullPath}"); + + try + { + return Response.Success( + "Asset info retrieved.", + GetAssetData(fullPath, generatePreview) + ); + } + catch (Exception e) + { + return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}"); + } + } + + /// + /// Retrieves components attached to a GameObject asset (like a Prefab). + /// + /// The asset path of the GameObject or Prefab. + /// A response object containing a list of component type names or an error. + private static object GetComponentsFromAsset(string path) + { + // 1. Validate input path + if (string.IsNullOrEmpty(path)) + return Response.Error("'path' is required for get_components."); + + // 2. Sanitize and check existence + string fullPath = AssetPathUtility.SanitizeAssetPath(path); + if (!AssetExists(fullPath)) + return Response.Error($"Asset not found at path: {fullPath}"); + + try + { + // 3. Load the asset + UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath( + fullPath + ); + if (asset == null) + return Response.Error($"Failed to load asset at path: {fullPath}"); + + // 4. Check if it's a GameObject (Prefabs load as GameObjects) + GameObject gameObject = asset as GameObject; + if (gameObject == null) + { + // Also check if it's *directly* a Component type (less common for primary assets) + Component componentAsset = asset as Component; + if (componentAsset != null) + { + // If the asset itself *is* a component, maybe return just its info? + // This is an edge case. Let's stick to GameObjects for now. + return Response.Error( + $"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject." + ); + } + return Response.Error( + $"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type." + ); + } + + // 5. Get components + Component[] components = gameObject.GetComponents(); + + // 6. Format component data + List componentList = components + .Select(comp => new + { + typeName = comp.GetType().FullName, + instanceID = comp.GetInstanceID(), + // TODO: Add more component-specific details here if needed in the future? + // Requires reflection or specific handling per component type. + }) + .ToList(); // Explicit cast for clarity if needed + + // 7. Return success response + return Response.Success( + $"Found {componentList.Count} component(s) on asset '{fullPath}'.", + componentList + ); + } + catch (Exception e) + { + Debug.LogError( + $"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}" + ); + return Response.Error( + $"Error getting components for asset '{fullPath}': {e.Message}" + ); + } + } + + // --- Internal Helpers --- + + /// + /// Ensures the asset path starts with "Assets/". + /// + /// + /// Checks if an asset exists at the given path (file or folder). + /// + private static bool AssetExists(string sanitizedPath) + { + // AssetDatabase APIs are generally preferred over raw File/Directory checks for assets. + // Check if it's a known asset GUID. + if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath))) + { + return true; + } + // AssetPathToGUID might not work for newly created folders not yet refreshed. + // Check directory explicitly for folders. + if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) + { + // Check if it's considered a *valid* folder by Unity + return AssetDatabase.IsValidFolder(sanitizedPath); + } + // Check file existence for non-folder assets. + if (File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) + { + return true; // Assume if file exists, it's an asset or will be imported + } + + return false; + // Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)); + } + + /// + /// Ensures the directory for a given asset path exists, creating it if necessary. + /// + private static void EnsureDirectoryExists(string directoryPath) + { + if (string.IsNullOrEmpty(directoryPath)) + return; + string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath); + if (!Directory.Exists(fullDirPath)) + { + Directory.CreateDirectory(fullDirPath); + AssetDatabase.Refresh(); // Let Unity know about the new folder + } + } + + /// + /// Applies properties from JObject to a Material. + /// + private static bool ApplyMaterialProperties(Material mat, JObject properties) + { + if (mat == null || properties == null) + return false; + bool modified = false; + + // Example: Set shader + if (properties["shader"]?.Type == JTokenType.String) + { + Shader newShader = Shader.Find(properties["shader"].ToString()); + if (newShader != null && mat.shader != newShader) + { + mat.shader = newShader; + modified = true; + } + } + // Example: Set color property + if (properties["color"] is JObject colorProps) + { + string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color + if (colorProps["value"] is JArray colArr && colArr.Count >= 3) + { + try + { + Color newColor = new Color( + colArr[0].ToObject(), + colArr[1].ToObject(), + colArr[2].ToObject(), + colArr.Count > 3 ? colArr[3].ToObject() : 1.0f + ); + if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) + { + mat.SetColor(propName, newColor); + modified = true; + } + } + catch (Exception ex) + { + Debug.LogWarning( + $"Error parsing color property '{propName}': {ex.Message}" + ); + } + } + } + else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py + { + string propName = "_Color"; + try + { + if (colorArr.Count >= 3) + { + Color newColor = new Color( + colorArr[0].ToObject(), + colorArr[1].ToObject(), + colorArr[2].ToObject(), + colorArr.Count > 3 ? colorArr[3].ToObject() : 1.0f + ); + if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) + { + mat.SetColor(propName, newColor); + modified = true; + } + } + } + catch (Exception ex) + { + Debug.LogWarning( + $"Error parsing color property '{propName}': {ex.Message}" + ); + } + } + // Example: Set float property + if (properties["float"] is JObject floatProps) + { + string propName = floatProps["name"]?.ToString(); + if ( + !string.IsNullOrEmpty(propName) && + (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer) + ) + { + try + { + float newVal = floatProps["value"].ToObject(); + if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal) + { + mat.SetFloat(propName, newVal); + modified = true; + } + } + catch (Exception ex) + { + Debug.LogWarning( + $"Error parsing float property '{propName}': {ex.Message}" + ); + } + } + } + // Example: Set texture property + if (properties["texture"] is JObject texProps) + { + string propName = texProps["name"]?.ToString() ?? "_MainTex"; // Default main texture + string texPath = texProps["path"]?.ToString(); + if (!string.IsNullOrEmpty(texPath)) + { + Texture newTex = AssetDatabase.LoadAssetAtPath( + AssetPathUtility.SanitizeAssetPath(texPath) + ); + if ( + newTex != null + && mat.HasProperty(propName) + && mat.GetTexture(propName) != newTex + ) + { + mat.SetTexture(propName, newTex); + modified = true; + } + else if (newTex == null) + { + Debug.LogWarning($"Texture not found at path: {texPath}"); + } + } + } + + // TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.) + return modified; + } + + /// + /// Applies properties from JObject to a PhysicsMaterial. + /// + private static bool ApplyPhysicsMaterialProperties(PhysicsMaterialType pmat, JObject properties) + { + if (pmat == null || properties == null) + return false; + bool modified = false; + + // Example: Set dynamic friction + if (properties["dynamicFriction"]?.Type == JTokenType.Float) + { + float dynamicFriction = properties["dynamicFriction"].ToObject(); + pmat.dynamicFriction = dynamicFriction; + modified = true; + } + + // Example: Set static friction + if (properties["staticFriction"]?.Type == JTokenType.Float) + { + float staticFriction = properties["staticFriction"].ToObject(); + pmat.staticFriction = staticFriction; + modified = true; + } + + // Example: Set bounciness + if (properties["bounciness"]?.Type == JTokenType.Float) + { + float bounciness = properties["bounciness"].ToObject(); + pmat.bounciness = bounciness; + modified = true; + } + + List averageList = new List { "ave", "Ave", "average", "Average" }; + List multiplyList = new List { "mul", "Mul", "mult", "Mult", "multiply", "Multiply" }; + List minimumList = new List { "min", "Min", "minimum", "Minimum" }; + List maximumList = new List { "max", "Max", "maximum", "Maximum" }; + + // Example: Set friction combine + if (properties["frictionCombine"]?.Type == JTokenType.String) + { + string frictionCombine = properties["frictionCombine"].ToString(); + if (averageList.Contains(frictionCombine)) + pmat.frictionCombine = PhysicsMaterialCombine.Average; + else if (multiplyList.Contains(frictionCombine)) + pmat.frictionCombine = PhysicsMaterialCombine.Multiply; + else if (minimumList.Contains(frictionCombine)) + pmat.frictionCombine = PhysicsMaterialCombine.Minimum; + else if (maximumList.Contains(frictionCombine)) + pmat.frictionCombine = PhysicsMaterialCombine.Maximum; + modified = true; + } + + // Example: Set bounce combine + if (properties["bounceCombine"]?.Type == JTokenType.String) + { + string bounceCombine = properties["bounceCombine"].ToString(); + if (averageList.Contains(bounceCombine)) + pmat.bounceCombine = PhysicsMaterialCombine.Average; + else if (multiplyList.Contains(bounceCombine)) + pmat.bounceCombine = PhysicsMaterialCombine.Multiply; + else if (minimumList.Contains(bounceCombine)) + pmat.bounceCombine = PhysicsMaterialCombine.Minimum; + else if (maximumList.Contains(bounceCombine)) + pmat.bounceCombine = PhysicsMaterialCombine.Maximum; + modified = true; + } + + return modified; + } + + /// + /// Generic helper to set properties on any UnityEngine.Object using reflection. + /// + private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties) + { + if (target == null || properties == null) + return false; + bool modified = false; + Type type = target.GetType(); + + foreach (var prop in properties.Properties()) + { + string propName = prop.Name; + JToken propValue = prop.Value; + if (SetPropertyOrField(target, propName, propValue, type)) + { + modified = true; + } + } + return modified; + } + + /// + /// Helper to set a property or field via reflection, handling basic types and Unity objects. + /// + private static bool SetPropertyOrField( + object target, + string memberName, + JToken value, + Type type = null + ) + { + type = type ?? target.GetType(); + System.Reflection.BindingFlags flags = + System.Reflection.BindingFlags.Public + | System.Reflection.BindingFlags.Instance + | System.Reflection.BindingFlags.IgnoreCase; + + try + { + System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags); + if (propInfo != null && propInfo.CanWrite) + { + object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType); + if ( + convertedValue != null + && !object.Equals(propInfo.GetValue(target), convertedValue) + ) + { + propInfo.SetValue(target, convertedValue); + return true; + } + } + else + { + System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags); + if (fieldInfo != null) + { + object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType); + if ( + convertedValue != null + && !object.Equals(fieldInfo.GetValue(target), convertedValue) + ) + { + fieldInfo.SetValue(target, convertedValue); + return true; + } + } + } + } + catch (Exception ex) + { + Debug.LogWarning( + $"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}" + ); + } + return false; + } + + /// + /// Simple JToken to Type conversion for common Unity types and primitives. + /// + private static object ConvertJTokenToType(JToken token, Type targetType) + { + try + { + if (token == null || token.Type == JTokenType.Null) + return null; + + if (targetType == typeof(string)) + return token.ToObject(); + if (targetType == typeof(int)) + return token.ToObject(); + if (targetType == typeof(float)) + return token.ToObject(); + if (targetType == typeof(bool)) + return token.ToObject(); + if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2) + return new Vector2(arrV2[0].ToObject(), arrV2[1].ToObject()); + if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3) + return new Vector3( + arrV3[0].ToObject(), + arrV3[1].ToObject(), + arrV3[2].ToObject() + ); + if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4) + return new Vector4( + arrV4[0].ToObject(), + arrV4[1].ToObject(), + arrV4[2].ToObject(), + arrV4[3].ToObject() + ); + if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4) + return new Quaternion( + arrQ[0].ToObject(), + arrQ[1].ToObject(), + arrQ[2].ToObject(), + arrQ[3].ToObject() + ); + if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA + return new Color( + arrC[0].ToObject(), + arrC[1].ToObject(), + arrC[2].ToObject(), + arrC.Count > 3 ? arrC[3].ToObject() : 1.0f + ); + if (targetType.IsEnum) + return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing + + // Handle loading Unity Objects (Materials, Textures, etc.) by path + if ( + typeof(UnityEngine.Object).IsAssignableFrom(targetType) + && token.Type == JTokenType.String + ) + { + string assetPath = AssetPathUtility.SanitizeAssetPath(token.ToString()); + UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath( + assetPath, + targetType + ); + if (loadedAsset == null) + { + Debug.LogWarning( + $"[ConvertJTokenToType] Could not load asset of type {targetType.Name} from path: {assetPath}" + ); + } + return loadedAsset; + } + + // Fallback: Try direct conversion (might work for other simple value types) + return token.ToObject(targetType); + } + catch (Exception ex) + { + Debug.LogWarning( + $"[ConvertJTokenToType] Could not convert JToken '{token}' (type {token.Type}) to type '{targetType.Name}': {ex.Message}" + ); + return null; + } + } + + + // --- Data Serialization --- + + /// + /// Creates a serializable representation of an asset. + /// + private static object GetAssetData(string path, bool generatePreview = false) + { + if (string.IsNullOrEmpty(path) || !AssetExists(path)) + return null; + + string guid = AssetDatabase.AssetPathToGUID(path); + Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path); + UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(path); + string previewBase64 = null; + int previewWidth = 0; + int previewHeight = 0; + + if (generatePreview && asset != null) + { + Texture2D preview = AssetPreview.GetAssetPreview(asset); + + if (preview != null) + { + try + { + // Ensure texture is readable for EncodeToPNG + // Creating a temporary readable copy is safer + RenderTexture rt = null; + Texture2D readablePreview = null; + RenderTexture previous = RenderTexture.active; + try + { + rt = RenderTexture.GetTemporary(preview.width, preview.height); + Graphics.Blit(preview, rt); + RenderTexture.active = rt; + readablePreview = new Texture2D(preview.width, preview.height, TextureFormat.RGB24, false); + readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); + readablePreview.Apply(); + + var pngData = readablePreview.EncodeToPNG(); + if (pngData != null && pngData.Length > 0) + { + previewBase64 = Convert.ToBase64String(pngData); + previewWidth = readablePreview.width; + previewHeight = readablePreview.height; + } + } + finally + { + RenderTexture.active = previous; + if (rt != null) RenderTexture.ReleaseTemporary(rt); + if (readablePreview != null) UnityEngine.Object.DestroyImmediate(readablePreview); + } + } + catch (Exception ex) + { + Debug.LogWarning( + $"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable." + ); + // Fallback: Try getting static preview if available? + // Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset); + } + } + else + { + Debug.LogWarning( + $"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?" + ); + } + } + + return new + { + path = path, + guid = guid, + assetType = assetType?.FullName ?? "Unknown", + name = Path.GetFileNameWithoutExtension(path), + fileName = Path.GetFileName(path), + isFolder = AssetDatabase.IsValidFolder(path), + instanceID = asset?.GetInstanceID() ?? 0, + lastWriteTimeUtc = File.GetLastWriteTimeUtc( + Path.Combine(Directory.GetCurrentDirectory(), path) + ) + .ToString("o"), // ISO 8601 + // --- Preview Data --- + previewBase64 = previewBase64, // PNG data as Base64 string + previewWidth = previewWidth, + previewHeight = previewHeight, + // TODO: Add more metadata? Importer settings? Dependencies? + }; + } + } +} diff --git a/MCPForUnity/Editor/Tools/ManageAsset.cs.meta b/MCPForUnity/Editor/Tools/ManageAsset.cs.meta new file mode 100644 index 00000000..3dbc2e2f --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageAsset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de90a1d9743a2874cb235cf0b83444b1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/ManageEditor.cs b/MCPForUnity/Editor/Tools/ManageEditor.cs new file mode 100644 index 00000000..f8255224 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageEditor.cs @@ -0,0 +1,645 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditorInternal; // Required for tag management +using UnityEditor.SceneManagement; +using UnityEngine; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Handles operations related to controlling and querying the Unity Editor state, + /// including managing Tags and Layers. + /// + [McpForUnityTool("manage_editor")] + public static class ManageEditor + { + // Constant for starting user layer index + private const int FirstUserLayerIndex = 8; + + // Constant for total layer count + private const int TotalLayerCount = 32; + + /// + /// Main handler for editor management actions. + /// + public static object HandleCommand(JObject @params) + { + string action = @params["action"]?.ToString().ToLower(); + // Parameters for specific actions + string tagName = @params["tagName"]?.ToString(); + string layerName = @params["layerName"]?.ToString(); + bool waitForCompletion = @params["waitForCompletion"]?.ToObject() ?? false; // Example - not used everywhere + + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + + // Route action + switch (action) + { + // Play Mode Control + case "play": + try + { + if (!EditorApplication.isPlaying) + { + EditorApplication.isPlaying = true; + return Response.Success("Entered play mode."); + } + return Response.Success("Already in play mode."); + } + catch (Exception e) + { + return Response.Error($"Error entering play mode: {e.Message}"); + } + case "pause": + try + { + if (EditorApplication.isPlaying) + { + EditorApplication.isPaused = !EditorApplication.isPaused; + return Response.Success( + EditorApplication.isPaused ? "Game paused." : "Game resumed." + ); + } + return Response.Error("Cannot pause/resume: Not in play mode."); + } + catch (Exception e) + { + return Response.Error($"Error pausing/resuming game: {e.Message}"); + } + case "stop": + try + { + if (EditorApplication.isPlaying) + { + EditorApplication.isPlaying = false; + return Response.Success("Exited play mode."); + } + return Response.Success("Already stopped (not in play mode)."); + } + catch (Exception e) + { + return Response.Error($"Error stopping play mode: {e.Message}"); + } + + // Editor State/Info + case "get_state": + return GetEditorState(); + case "get_project_root": + return GetProjectRoot(); + case "get_windows": + return GetEditorWindows(); + case "get_active_tool": + return GetActiveTool(); + case "get_selection": + return GetSelection(); + case "get_prefab_stage": + return GetPrefabStageInfo(); + case "set_active_tool": + string toolName = @params["toolName"]?.ToString(); + if (string.IsNullOrEmpty(toolName)) + return Response.Error("'toolName' parameter required for set_active_tool."); + return SetActiveTool(toolName); + + // Tag Management + case "add_tag": + if (string.IsNullOrEmpty(tagName)) + return Response.Error("'tagName' parameter required for add_tag."); + return AddTag(tagName); + case "remove_tag": + if (string.IsNullOrEmpty(tagName)) + return Response.Error("'tagName' parameter required for remove_tag."); + return RemoveTag(tagName); + case "get_tags": + return GetTags(); // Helper to list current tags + + // Layer Management + case "add_layer": + if (string.IsNullOrEmpty(layerName)) + return Response.Error("'layerName' parameter required for add_layer."); + return AddLayer(layerName); + case "remove_layer": + if (string.IsNullOrEmpty(layerName)) + return Response.Error("'layerName' parameter required for remove_layer."); + return RemoveLayer(layerName); + case "get_layers": + return GetLayers(); // Helper to list current layers + + // --- Settings (Example) --- + // case "set_resolution": + // int? width = @params["width"]?.ToObject(); + // int? height = @params["height"]?.ToObject(); + // if (!width.HasValue || !height.HasValue) return Response.Error("'width' and 'height' parameters required."); + // return SetGameViewResolution(width.Value, height.Value); + // case "set_quality": + // // Handle string name or int index + // return SetQualityLevel(@params["qualityLevel"]); + + default: + return Response.Error( + $"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, get_prefab_stage, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers." + ); + } + } + + // --- Editor State/Info Methods --- + private static object GetEditorState() + { + try + { + var state = new + { + isPlaying = EditorApplication.isPlaying, + isPaused = EditorApplication.isPaused, + isCompiling = EditorApplication.isCompiling, + isUpdating = EditorApplication.isUpdating, + applicationPath = EditorApplication.applicationPath, + applicationContentsPath = EditorApplication.applicationContentsPath, + timeSinceStartup = EditorApplication.timeSinceStartup, + }; + return Response.Success("Retrieved editor state.", state); + } + catch (Exception e) + { + return Response.Error($"Error getting editor state: {e.Message}"); + } + } + + private static object GetProjectRoot() + { + try + { + // Application.dataPath points to /Assets + string assetsPath = Application.dataPath.Replace('\\', '/'); + string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/'); + if (string.IsNullOrEmpty(projectRoot)) + { + return Response.Error("Could not determine project root from Application.dataPath"); + } + return Response.Success("Project root resolved.", new { projectRoot }); + } + catch (Exception e) + { + return Response.Error($"Error getting project root: {e.Message}"); + } + } + + private static object GetEditorWindows() + { + try + { + // Get all types deriving from EditorWindow + var windowTypes = AppDomain + .CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsSubclassOf(typeof(EditorWindow))) + .ToList(); + + var openWindows = new List(); + + // Find currently open instances + // Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows + EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll(); + + foreach (EditorWindow window in allWindows) + { + if (window == null) + continue; // Skip potentially destroyed windows + + try + { + openWindows.Add( + new + { + title = window.titleContent.text, + typeName = window.GetType().FullName, + isFocused = EditorWindow.focusedWindow == window, + position = new + { + x = window.position.x, + y = window.position.y, + width = window.position.width, + height = window.position.height, + }, + instanceID = window.GetInstanceID(), + } + ); + } + catch (Exception ex) + { + Debug.LogWarning( + $"Could not get info for window {window.GetType().Name}: {ex.Message}" + ); + } + } + + return Response.Success("Retrieved list of open editor windows.", openWindows); + } + catch (Exception e) + { + return Response.Error($"Error getting editor windows: {e.Message}"); + } + } + + private static object GetPrefabStageInfo() + { + try + { + PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); + if (stage == null) + { + return Response.Success + ("No prefab stage is currently open.", new { isOpen = false }); + } + + return Response.Success( + "Prefab stage info retrieved.", + new + { + isOpen = true, + assetPath = stage.assetPath, + prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null, + mode = stage.mode.ToString(), + isDirty = stage.scene.isDirty + } + ); + } + catch (Exception e) + { + return Response.Error($"Error getting prefab stage info: {e.Message}"); + } + } + + private static object GetActiveTool() + { + try + { + Tool currentTool = UnityEditor.Tools.current; + string toolName = currentTool.ToString(); // Enum to string + bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active + string activeToolName = customToolActive + ? EditorTools.GetActiveToolName() + : toolName; // Get custom name if needed + + var toolInfo = new + { + activeTool = activeToolName, + isCustom = customToolActive, + pivotMode = UnityEditor.Tools.pivotMode.ToString(), + pivotRotation = UnityEditor.Tools.pivotRotation.ToString(), + handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity + handlePosition = UnityEditor.Tools.handlePosition, + }; + + return Response.Success("Retrieved active tool information.", toolInfo); + } + catch (Exception e) + { + return Response.Error($"Error getting active tool: {e.Message}"); + } + } + + private static object SetActiveTool(string toolName) + { + try + { + Tool targetTool; + if (Enum.TryParse(toolName, true, out targetTool)) // Case-insensitive parse + { + // Check if it's a valid built-in tool + if (targetTool != Tool.None && targetTool <= Tool.Custom) // Tool.Custom is the last standard tool + { + UnityEditor.Tools.current = targetTool; + return Response.Success($"Set active tool to '{targetTool}'."); + } + else + { + return Response.Error( + $"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid." + ); + } + } + else + { + // Potentially try activating a custom tool by name here if needed + // This often requires specific editor scripting knowledge for that tool. + return Response.Error( + $"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom)." + ); + } + } + catch (Exception e) + { + return Response.Error($"Error setting active tool: {e.Message}"); + } + } + + private static object GetSelection() + { + try + { + var selectionInfo = new + { + activeObject = Selection.activeObject?.name, + activeGameObject = Selection.activeGameObject?.name, + activeTransform = Selection.activeTransform?.name, + activeInstanceID = Selection.activeInstanceID, + count = Selection.count, + objects = Selection + .objects.Select(obj => new + { + name = obj?.name, + type = obj?.GetType().FullName, + instanceID = obj?.GetInstanceID(), + }) + .ToList(), + gameObjects = Selection + .gameObjects.Select(go => new + { + name = go?.name, + instanceID = go?.GetInstanceID(), + }) + .ToList(), + assetGUIDs = Selection.assetGUIDs, // GUIDs for selected assets in Project view + }; + + return Response.Success("Retrieved current selection details.", selectionInfo); + } + catch (Exception e) + { + return Response.Error($"Error getting selection: {e.Message}"); + } + } + + // --- Tag Management Methods --- + + private static object AddTag(string tagName) + { + if (string.IsNullOrWhiteSpace(tagName)) + return Response.Error("Tag name cannot be empty or whitespace."); + + // Check if tag already exists + if (InternalEditorUtility.tags.Contains(tagName)) + { + return Response.Error($"Tag '{tagName}' already exists."); + } + + try + { + // Add the tag using the internal utility + InternalEditorUtility.AddTag(tagName); + // Force save assets to ensure the change persists in the TagManager asset + AssetDatabase.SaveAssets(); + return Response.Success($"Tag '{tagName}' added successfully."); + } + catch (Exception e) + { + return Response.Error($"Failed to add tag '{tagName}': {e.Message}"); + } + } + + private static object RemoveTag(string tagName) + { + if (string.IsNullOrWhiteSpace(tagName)) + return Response.Error("Tag name cannot be empty or whitespace."); + if (tagName.Equals("Untagged", StringComparison.OrdinalIgnoreCase)) + return Response.Error("Cannot remove the built-in 'Untagged' tag."); + + // Check if tag exists before attempting removal + if (!InternalEditorUtility.tags.Contains(tagName)) + { + return Response.Error($"Tag '{tagName}' does not exist."); + } + + try + { + // Remove the tag using the internal utility + InternalEditorUtility.RemoveTag(tagName); + // Force save assets + AssetDatabase.SaveAssets(); + return Response.Success($"Tag '{tagName}' removed successfully."); + } + catch (Exception e) + { + // Catch potential issues if the tag is somehow in use or removal fails + return Response.Error($"Failed to remove tag '{tagName}': {e.Message}"); + } + } + + private static object GetTags() + { + try + { + string[] tags = InternalEditorUtility.tags; + return Response.Success("Retrieved current tags.", tags); + } + catch (Exception e) + { + return Response.Error($"Failed to retrieve tags: {e.Message}"); + } + } + + // --- Layer Management Methods --- + + private static object AddLayer(string layerName) + { + if (string.IsNullOrWhiteSpace(layerName)) + return Response.Error("Layer name cannot be empty or whitespace."); + + // Access the TagManager asset + SerializedObject tagManager = GetTagManager(); + if (tagManager == null) + return Response.Error("Could not access TagManager asset."); + + SerializedProperty layersProp = tagManager.FindProperty("layers"); + if (layersProp == null || !layersProp.isArray) + return Response.Error("Could not find 'layers' property in TagManager."); + + // Check if layer name already exists (case-insensitive check recommended) + for (int i = 0; i < TotalLayerCount; i++) + { + SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); + if ( + layerSP != null + && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) + ) + { + return Response.Error($"Layer '{layerName}' already exists at index {i}."); + } + } + + // Find the first empty user layer slot (indices 8 to 31) + int firstEmptyUserLayer = -1; + for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) + { + SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); + if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue)) + { + firstEmptyUserLayer = i; + break; + } + } + + if (firstEmptyUserLayer == -1) + { + return Response.Error("No empty User Layer slots available (8-31 are full)."); + } + + // Assign the name to the found slot + try + { + SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( + firstEmptyUserLayer + ); + targetLayerSP.stringValue = layerName; + // Apply the changes to the TagManager asset + tagManager.ApplyModifiedProperties(); + // Save assets to make sure it's written to disk + AssetDatabase.SaveAssets(); + return Response.Success( + $"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}." + ); + } + catch (Exception e) + { + return Response.Error($"Failed to add layer '{layerName}': {e.Message}"); + } + } + + private static object RemoveLayer(string layerName) + { + if (string.IsNullOrWhiteSpace(layerName)) + return Response.Error("Layer name cannot be empty or whitespace."); + + // Access the TagManager asset + SerializedObject tagManager = GetTagManager(); + if (tagManager == null) + return Response.Error("Could not access TagManager asset."); + + SerializedProperty layersProp = tagManager.FindProperty("layers"); + if (layersProp == null || !layersProp.isArray) + return Response.Error("Could not find 'layers' property in TagManager."); + + // Find the layer by name (must be user layer) + int layerIndexToRemove = -1; + for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) // Start from user layers + { + SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); + // Case-insensitive comparison is safer + if ( + layerSP != null + && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) + ) + { + layerIndexToRemove = i; + break; + } + } + + if (layerIndexToRemove == -1) + { + return Response.Error($"User layer '{layerName}' not found."); + } + + // Clear the name for that index + try + { + SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( + layerIndexToRemove + ); + targetLayerSP.stringValue = string.Empty; // Set to empty string to remove + // Apply the changes + tagManager.ApplyModifiedProperties(); + // Save assets + AssetDatabase.SaveAssets(); + return Response.Success( + $"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully." + ); + } + catch (Exception e) + { + return Response.Error($"Failed to remove layer '{layerName}': {e.Message}"); + } + } + + private static object GetLayers() + { + try + { + var layers = new Dictionary(); + for (int i = 0; i < TotalLayerCount; i++) + { + string layerName = LayerMask.LayerToName(i); + if (!string.IsNullOrEmpty(layerName)) // Only include layers that have names + { + layers.Add(i, layerName); + } + } + return Response.Success("Retrieved current named layers.", layers); + } + catch (Exception e) + { + return Response.Error($"Failed to retrieve layers: {e.Message}"); + } + } + + // --- Helper Methods --- + + /// + /// Gets the SerializedObject for the TagManager asset. + /// + private static SerializedObject GetTagManager() + { + try + { + // Load the TagManager asset from the ProjectSettings folder + UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath( + "ProjectSettings/TagManager.asset" + ); + if (tagManagerAssets == null || tagManagerAssets.Length == 0) + { + Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings."); + return null; + } + // The first object in the asset file should be the TagManager + return new SerializedObject(tagManagerAssets[0]); + } + catch (Exception e) + { + Debug.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}"); + return null; + } + } + + // --- Example Implementations for Settings --- + /* + private static object SetGameViewResolution(int width, int height) { ... } + private static object SetQualityLevel(JToken qualityLevelToken) { ... } + */ + } + + // Helper class to get custom tool names (remains the same) + internal static class EditorTools + { + public static string GetActiveToolName() + { + // This is a placeholder. Real implementation depends on how custom tools + // are registered and tracked in the specific Unity project setup. + // It might involve checking static variables, calling methods on specific tool managers, etc. + if (UnityEditor.Tools.current == Tool.Custom) + { + // Example: Check a known custom tool manager + // if (MyCustomToolManager.IsActive) return MyCustomToolManager.ActiveToolName; + return "Unknown Custom Tool"; + } + return UnityEditor.Tools.current.ToString(); + } + } +} diff --git a/MCPForUnity/Editor/Tools/ManageEditor.cs.meta b/MCPForUnity/Editor/Tools/ManageEditor.cs.meta new file mode 100644 index 00000000..8b55fb87 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43ac60aa36b361b4dbe4a038ae9f35c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/ManageGameObject.cs b/MCPForUnity/Editor/Tools/ManageGameObject.cs new file mode 100644 index 00000000..40504a87 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageGameObject.cs @@ -0,0 +1,2548 @@ +#nullable disable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; // Added for JsonSerializationException +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.Compilation; // For CompilationPipeline +using UnityEditor.SceneManagement; +using UnityEditorInternal; +using UnityEngine; +using UnityEngine.SceneManagement; +using MCPForUnity.Editor.Helpers; // For Response class +using MCPForUnity.Runtime.Serialization; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Handles GameObject manipulation within the current scene (CRUD, find, components). + /// + [McpForUnityTool("manage_gameobject")] + public static class ManageGameObject + { + // Shared JsonSerializer to avoid per-call allocation overhead + private static readonly JsonSerializer InputSerializer = JsonSerializer.Create(new JsonSerializerSettings + { + Converters = new List + { + new Vector3Converter(), + new Vector2Converter(), + new QuaternionConverter(), + new ColorConverter(), + new RectConverter(), + new BoundsConverter(), + new UnityEngineObjectConverter() + } + }); + + // --- Main Handler --- + + public static object HandleCommand(JObject @params) + { + if (@params == null) + { + return Response.Error("Parameters cannot be null."); + } + + string action = @params["action"]?.ToString().ToLower(); + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + + // Parameters used by various actions + JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID) + string searchMethod = @params["searchMethod"]?.ToString().ToLower(); + + // Get common parameters (consolidated) + string name = @params["name"]?.ToString(); + string tag = @params["tag"]?.ToString(); + string layer = @params["layer"]?.ToString(); + JToken parentToken = @params["parent"]; + + // --- Add parameter for controlling non-public field inclusion --- + bool includeNonPublicSerialized = @params["includeNonPublicSerialized"]?.ToObject() ?? true; // Default to true + // --- End add parameter --- + + // --- Prefab Redirection Check --- + string targetPath = + targetToken?.Type == JTokenType.String ? targetToken.ToString() : null; + if ( + !string.IsNullOrEmpty(targetPath) + && targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) + ) + { + // Allow 'create' (instantiate), 'find' (?), 'get_components' (?) + if (action == "modify" || action == "set_component_property") + { + Debug.Log( + $"[ManageGameObject->ManageAsset] Redirecting action '{action}' for prefab '{targetPath}' to ManageAsset." + ); + // Prepare params for ManageAsset.ModifyAsset + JObject assetParams = new JObject(); + assetParams["action"] = "modify"; // ManageAsset uses "modify" + assetParams["path"] = targetPath; + + // Extract properties. + // For 'set_component_property', combine componentName and componentProperties. + // For 'modify', directly use componentProperties. + JObject properties = null; + if (action == "set_component_property") + { + string compName = @params["componentName"]?.ToString(); + JObject compProps = @params["componentProperties"]?[compName] as JObject; // Handle potential nesting + if (string.IsNullOrEmpty(compName)) + return Response.Error( + "Missing 'componentName' for 'set_component_property' on prefab." + ); + if (compProps == null) + return Response.Error( + $"Missing or invalid 'componentProperties' for component '{compName}' for 'set_component_property' on prefab." + ); + + properties = new JObject(); + properties[compName] = compProps; + } + else // action == "modify" + { + properties = @params["componentProperties"] as JObject; + if (properties == null) + return Response.Error( + "Missing 'componentProperties' for 'modify' action on prefab." + ); + } + + assetParams["properties"] = properties; + + // Call ManageAsset handler + return ManageAsset.HandleCommand(assetParams); + } + else if ( + action == "delete" + || action == "add_component" + || action == "remove_component" + || action == "get_components" + ) // Added get_components here too + { + // Explicitly block other modifications on the prefab asset itself via manage_gameobject + return Response.Error( + $"Action '{action}' on a prefab asset ('{targetPath}') should be performed using the 'manage_asset' command." + ); + } + // Allow 'create' (instantiation) and 'find' to proceed, although finding a prefab asset by path might be less common via manage_gameobject. + // No specific handling needed here, the code below will run. + } + // --- End Prefab Redirection Check --- + + try + { + switch (action) + { + case "create": + return CreateGameObject(@params); + case "modify": + return ModifyGameObject(@params, targetToken, searchMethod); + case "delete": + return DeleteGameObject(targetToken, searchMethod); + case "find": + return FindGameObjects(@params, targetToken, searchMethod); + case "get_components": + string getCompTarget = targetToken?.ToString(); // Expect name, path, or ID string + if (getCompTarget == null) + return Response.Error( + "'target' parameter required for get_components." + ); + // Pass the includeNonPublicSerialized flag here + return GetComponentsFromTarget(getCompTarget, searchMethod, includeNonPublicSerialized); + case "get_component": + string getSingleCompTarget = targetToken?.ToString(); + if (getSingleCompTarget == null) + return Response.Error( + "'target' parameter required for get_component." + ); + string componentName = @params["componentName"]?.ToString(); + if (string.IsNullOrEmpty(componentName)) + return Response.Error( + "'componentName' parameter required for get_component." + ); + return GetSingleComponentFromTarget(getSingleCompTarget, searchMethod, componentName, includeNonPublicSerialized); + case "add_component": + return AddComponentToTarget(@params, targetToken, searchMethod); + case "remove_component": + return RemoveComponentFromTarget(@params, targetToken, searchMethod); + case "set_component_property": + return SetComponentPropertyOnTarget(@params, targetToken, searchMethod); + + default: + return Response.Error($"Unknown action: '{action}'."); + } + } + catch (Exception e) + { + Debug.LogError($"[ManageGameObject] Action '{action}' failed: {e}"); + return Response.Error($"Internal error processing action '{action}': {e.Message}"); + } + } + + // --- Action Implementations --- + + private static object CreateGameObject(JObject @params) + { + string name = @params["name"]?.ToString(); + if (string.IsNullOrEmpty(name)) + { + return Response.Error("'name' parameter is required for 'create' action."); + } + + // Get prefab creation parameters + bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject() ?? false; + string prefabPath = @params["prefabPath"]?.ToString(); + string tag = @params["tag"]?.ToString(); // Get tag for creation + string primitiveType = @params["primitiveType"]?.ToString(); // Keep primitiveType check + GameObject newGo = null; // Initialize as null + + // --- Try Instantiating Prefab First --- + string originalPrefabPath = prefabPath; // Keep original for messages + if (!string.IsNullOrEmpty(prefabPath)) + { + // If no extension, search for the prefab by name + if ( + !prefabPath.Contains("/") + && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) + ) + { + string prefabNameOnly = prefabPath; + Debug.Log( + $"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'" + ); + string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}"); + if (guids.Length == 0) + { + return Response.Error( + $"Prefab named '{prefabNameOnly}' not found anywhere in the project." + ); + } + else if (guids.Length > 1) + { + string foundPaths = string.Join( + ", ", + guids.Select(g => AssetDatabase.GUIDToAssetPath(g)) + ); + return Response.Error( + $"Multiple prefabs found matching name '{prefabNameOnly}': {foundPaths}. Please provide a more specific path." + ); + } + else // Exactly one found + { + prefabPath = AssetDatabase.GUIDToAssetPath(guids[0]); // Update prefabPath with the full path + Debug.Log( + $"[ManageGameObject.Create] Found unique prefab at path: '{prefabPath}'" + ); + } + } + else if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) + { + // If it looks like a path but doesn't end with .prefab, assume user forgot it and append it. + Debug.LogWarning( + $"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending." + ); + prefabPath += ".prefab"; + // Note: This path might still not exist, AssetDatabase.LoadAssetAtPath will handle that. + } + // The logic above now handles finding or assuming the .prefab extension. + + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); + if (prefabAsset != null) + { + try + { + // Instantiate the prefab, initially place it at the root + // Parent will be set later if specified + newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; + + if (newGo == null) + { + // This might happen if the asset exists but isn't a valid GameObject prefab somehow + Debug.LogError( + $"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject." + ); + return Response.Error( + $"Failed to instantiate prefab at '{prefabPath}'." + ); + } + // Name the instance based on the 'name' parameter, not the prefab's default name + if (!string.IsNullOrEmpty(name)) + { + newGo.name = name; + } + // Register Undo for prefab instantiation + Undo.RegisterCreatedObjectUndo( + newGo, + $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'" + ); + Debug.Log( + $"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'." + ); + } + catch (Exception e) + { + return Response.Error( + $"Error instantiating prefab '{prefabPath}': {e.Message}" + ); + } + } + else + { + // Only return error if prefabPath was specified but not found. + // If prefabPath was empty/null, we proceed to create primitive/empty. + Debug.LogWarning( + $"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified." + ); + // Do not return error here, allow fallback to primitive/empty creation + } + } + + // --- Fallback: Create Primitive or Empty GameObject --- + bool createdNewObject = false; // Flag to track if we created (not instantiated) + if (newGo == null) // Only proceed if prefab instantiation didn't happen + { + if (!string.IsNullOrEmpty(primitiveType)) + { + try + { + PrimitiveType type = (PrimitiveType) + Enum.Parse(typeof(PrimitiveType), primitiveType, true); + newGo = GameObject.CreatePrimitive(type); + // Set name *after* creation for primitives + if (!string.IsNullOrEmpty(name)) + { + newGo.name = name; + } + else + { + UnityEngine.Object.DestroyImmediate(newGo); // cleanup leak + return Response.Error( + "'name' parameter is required when creating a primitive." + ); + } + createdNewObject = true; + } + catch (ArgumentException) + { + return Response.Error( + $"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}" + ); + } + catch (Exception e) + { + return Response.Error( + $"Failed to create primitive '{primitiveType}': {e.Message}" + ); + } + } + else // Create empty GameObject + { + if (string.IsNullOrEmpty(name)) + { + return Response.Error( + "'name' parameter is required for 'create' action when not instantiating a prefab or creating a primitive." + ); + } + newGo = new GameObject(name); + createdNewObject = true; + } + // Record creation for Undo *only* if we created a new object + if (createdNewObject) + { + Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'"); + } + } + // --- Common Setup (Parent, Transform, Tag, Components) - Applied AFTER object exists --- + if (newGo == null) + { + // Should theoretically not happen if logic above is correct, but safety check. + return Response.Error("Failed to create or instantiate the GameObject."); + } + + // Record potential changes to the existing prefab instance or the new GO + // Record transform separately in case parent changes affect it + Undo.RecordObject(newGo.transform, "Set GameObject Transform"); + Undo.RecordObject(newGo, "Set GameObject Properties"); + + // Set Parent + JToken parentToken = @params["parent"]; + if (parentToken != null) + { + GameObject parentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path"); // Flexible parent finding + if (parentGo == null) + { + UnityEngine.Object.DestroyImmediate(newGo); // Clean up created object + return Response.Error($"Parent specified ('{parentToken}') but not found."); + } + newGo.transform.SetParent(parentGo.transform, true); // worldPositionStays = true + } + + // Set Transform + Vector3? position = ParseVector3(@params["position"] as JArray); + Vector3? rotation = ParseVector3(@params["rotation"] as JArray); + Vector3? scale = ParseVector3(@params["scale"] as JArray); + + if (position.HasValue) + newGo.transform.localPosition = position.Value; + if (rotation.HasValue) + newGo.transform.localEulerAngles = rotation.Value; + if (scale.HasValue) + newGo.transform.localScale = scale.Value; + + // Set Tag (added for create action) + if (!string.IsNullOrEmpty(tag)) + { + // Similar logic as in ModifyGameObject for setting/creating tags + string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; + try + { + newGo.tag = tagToSet; + } + catch (UnityException ex) + { + if (ex.Message.Contains("is not defined")) + { + Debug.LogWarning( + $"[ManageGameObject.Create] Tag '{tagToSet}' not found. Attempting to create it." + ); + try + { + InternalEditorUtility.AddTag(tagToSet); + newGo.tag = tagToSet; // Retry + Debug.Log( + $"[ManageGameObject.Create] Tag '{tagToSet}' created and assigned successfully." + ); + } + catch (Exception innerEx) + { + UnityEngine.Object.DestroyImmediate(newGo); // Clean up + return Response.Error( + $"Failed to create or assign tag '{tagToSet}' during creation: {innerEx.Message}." + ); + } + } + else + { + UnityEngine.Object.DestroyImmediate(newGo); // Clean up + return Response.Error( + $"Failed to set tag to '{tagToSet}' during creation: {ex.Message}." + ); + } + } + } + + // Set Layer (new for create action) + string layerName = @params["layer"]?.ToString(); + if (!string.IsNullOrEmpty(layerName)) + { + int layerId = LayerMask.NameToLayer(layerName); + if (layerId != -1) + { + newGo.layer = layerId; + } + else + { + Debug.LogWarning( + $"[ManageGameObject.Create] Layer '{layerName}' not found. Using default layer." + ); + } + } + + // Add Components + if (@params["componentsToAdd"] is JArray componentsToAddArray) + { + foreach (var compToken in componentsToAddArray) + { + string typeName = null; + JObject properties = null; + + if (compToken.Type == JTokenType.String) + { + typeName = compToken.ToString(); + } + else if (compToken is JObject compObj) + { + typeName = compObj["typeName"]?.ToString(); + properties = compObj["properties"] as JObject; + } + + if (!string.IsNullOrEmpty(typeName)) + { + var addResult = AddComponentInternal(newGo, typeName, properties); + if (addResult != null) // Check if AddComponentInternal returned an error object + { + UnityEngine.Object.DestroyImmediate(newGo); // Clean up + return addResult; // Return the error response + } + } + else + { + Debug.LogWarning( + $"[ManageGameObject] Invalid component format in componentsToAdd: {compToken}" + ); + } + } + } + + // Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true + GameObject finalInstance = newGo; // Use this for selection and return data + if (createdNewObject && saveAsPrefab) + { + string finalPrefabPath = prefabPath; // Use a separate variable for saving path + // This check should now happen *before* attempting to save + if (string.IsNullOrEmpty(finalPrefabPath)) + { + // Clean up the created object before returning error + UnityEngine.Object.DestroyImmediate(newGo); + return Response.Error( + "'prefabPath' is required when 'saveAsPrefab' is true and creating a new object." + ); + } + // Ensure the *saving* path ends with .prefab + if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) + { + Debug.Log( + $"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'" + ); + finalPrefabPath += ".prefab"; + } + + try + { + // Ensure directory exists using the final saving path + string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath); + if ( + !string.IsNullOrEmpty(directoryPath) + && !System.IO.Directory.Exists(directoryPath) + ) + { + System.IO.Directory.CreateDirectory(directoryPath); + AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder + Debug.Log( + $"[ManageGameObject.Create] Created directory for prefab: {directoryPath}" + ); + } + // Use SaveAsPrefabAssetAndConnect with the final saving path + finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect( + newGo, + finalPrefabPath, + InteractionMode.UserAction + ); + + if (finalInstance == null) + { + // Destroy the original if saving failed somehow (shouldn't usually happen if path is valid) + UnityEngine.Object.DestroyImmediate(newGo); + return Response.Error( + $"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions." + ); + } + Debug.Log( + $"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected." + ); + // Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it. + // EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect + } + catch (Exception e) + { + // Clean up the instance if prefab saving fails + UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt + return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}"); + } + } + + // Select the instance in the scene (either prefab instance or newly created/saved one) + Selection.activeGameObject = finalInstance; + + // Determine appropriate success message using the potentially updated or original path + string messagePrefabPath = + finalInstance == null + ? originalPrefabPath + : AssetDatabase.GetAssetPath( + PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) + ?? (UnityEngine.Object)finalInstance + ); + string successMessage; + if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath)) // Instantiated existing prefab + { + successMessage = + $"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'."; + } + else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath)) // Created new and saved as prefab + { + successMessage = + $"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'."; + } + else // Created new primitive or empty GO, didn't save as prefab + { + successMessage = + $"GameObject '{finalInstance.name}' created successfully in scene."; + } + + // Use the new serializer helper + //return Response.Success(successMessage, GetGameObjectData(finalInstance)); + return Response.Success(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance)); + } + + private static object ModifyGameObject( + JObject @params, + JToken targetToken, + string searchMethod + ) + { + GameObject targetGo = FindObjectInternal(targetToken, searchMethod); + if (targetGo == null) + { + return Response.Error( + $"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'." + ); + } + + // Record state for Undo *before* modifications + Undo.RecordObject(targetGo.transform, "Modify GameObject Transform"); + Undo.RecordObject(targetGo, "Modify GameObject Properties"); + + bool modified = false; + + // Rename (using consolidated 'name' parameter) + string name = @params["name"]?.ToString(); + if (!string.IsNullOrEmpty(name) && targetGo.name != name) + { + targetGo.name = name; + modified = true; + } + + // Change Parent (using consolidated 'parent' parameter) + JToken parentToken = @params["parent"]; + if (parentToken != null) + { + GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path"); + // Check for hierarchy loops + if ( + newParentGo == null + && !( + parentToken.Type == JTokenType.Null + || ( + parentToken.Type == JTokenType.String + && string.IsNullOrEmpty(parentToken.ToString()) + ) + ) + ) + { + return Response.Error($"New parent ('{parentToken}') not found."); + } + if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform)) + { + return Response.Error( + $"Cannot parent '{targetGo.name}' to '{newParentGo.name}', as it would create a hierarchy loop." + ); + } + if (targetGo.transform.parent != (newParentGo?.transform)) + { + targetGo.transform.SetParent(newParentGo?.transform, true); // worldPositionStays = true + modified = true; + } + } + + // Set Active State + bool? setActive = @params["setActive"]?.ToObject(); + if (setActive.HasValue && targetGo.activeSelf != setActive.Value) + { + targetGo.SetActive(setActive.Value); + modified = true; + } + + // Change Tag (using consolidated 'tag' parameter) + string tag = @params["tag"]?.ToString(); + // Only attempt to change tag if a non-null tag is provided and it's different from the current one. + // Allow setting an empty string to remove the tag (Unity uses "Untagged"). + if (tag != null && targetGo.tag != tag) + { + // Ensure the tag is not empty, if empty, it means "Untagged" implicitly + string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; + try + { + targetGo.tag = tagToSet; + modified = true; + } + catch (UnityException ex) + { + // Check if the error is specifically because the tag doesn't exist + if (ex.Message.Contains("is not defined")) + { + Debug.LogWarning( + $"[ManageGameObject] Tag '{tagToSet}' not found. Attempting to create it." + ); + try + { + // Attempt to create the tag using internal utility + InternalEditorUtility.AddTag(tagToSet); + // Wait a frame maybe? Not strictly necessary but sometimes helps editor updates. + // yield return null; // Cannot yield here, editor script limitation + + // Retry setting the tag immediately after creation + targetGo.tag = tagToSet; + modified = true; + Debug.Log( + $"[ManageGameObject] Tag '{tagToSet}' created and assigned successfully." + ); + } + catch (Exception innerEx) + { + // Handle failure during tag creation or the second assignment attempt + Debug.LogError( + $"[ManageGameObject] Failed to create or assign tag '{tagToSet}' after attempting creation: {innerEx.Message}" + ); + return Response.Error( + $"Failed to create or assign tag '{tagToSet}': {innerEx.Message}. Check Tag Manager and permissions." + ); + } + } + else + { + // If the exception was for a different reason, return the original error + return Response.Error($"Failed to set tag to '{tagToSet}': {ex.Message}."); + } + } + } + + // Change Layer (using consolidated 'layer' parameter) + string layerName = @params["layer"]?.ToString(); + if (!string.IsNullOrEmpty(layerName)) + { + int layerId = LayerMask.NameToLayer(layerName); + if (layerId == -1 && layerName != "Default") + { + return Response.Error( + $"Invalid layer specified: '{layerName}'. Use a valid layer name." + ); + } + if (layerId != -1 && targetGo.layer != layerId) + { + targetGo.layer = layerId; + modified = true; + } + } + + // Transform Modifications + Vector3? position = ParseVector3(@params["position"] as JArray); + Vector3? rotation = ParseVector3(@params["rotation"] as JArray); + Vector3? scale = ParseVector3(@params["scale"] as JArray); + + if (position.HasValue && targetGo.transform.localPosition != position.Value) + { + targetGo.transform.localPosition = position.Value; + modified = true; + } + if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value) + { + targetGo.transform.localEulerAngles = rotation.Value; + modified = true; + } + if (scale.HasValue && targetGo.transform.localScale != scale.Value) + { + targetGo.transform.localScale = scale.Value; + modified = true; + } + + // --- Component Modifications --- + // Note: These might need more specific Undo recording per component + + // Remove Components + if (@params["componentsToRemove"] is JArray componentsToRemoveArray) + { + foreach (var compToken in componentsToRemoveArray) + { + // ... (parsing logic as in CreateGameObject) ... + string typeName = compToken.ToString(); + if (!string.IsNullOrEmpty(typeName)) + { + var removeResult = RemoveComponentInternal(targetGo, typeName); + if (removeResult != null) + return removeResult; // Return error if removal failed + modified = true; + } + } + } + + // Add Components (similar to create) + if (@params["componentsToAdd"] is JArray componentsToAddArrayModify) + { + foreach (var compToken in componentsToAddArrayModify) + { + string typeName = null; + JObject properties = null; + if (compToken.Type == JTokenType.String) + typeName = compToken.ToString(); + else if (compToken is JObject compObj) + { + typeName = compObj["typeName"]?.ToString(); + properties = compObj["properties"] as JObject; + } + + if (!string.IsNullOrEmpty(typeName)) + { + var addResult = AddComponentInternal(targetGo, typeName, properties); + if (addResult != null) + return addResult; + modified = true; + } + } + } + + // Set Component Properties + var componentErrors = new List(); + if (@params["componentProperties"] is JObject componentPropertiesObj) + { + foreach (var prop in componentPropertiesObj.Properties()) + { + string compName = prop.Name; + JObject propertiesToSet = prop.Value as JObject; + if (propertiesToSet != null) + { + var setResult = SetComponentPropertiesInternal( + targetGo, + compName, + propertiesToSet + ); + if (setResult != null) + { + componentErrors.Add(setResult); + } + else + { + modified = true; + } + } + } + } + + // Return component errors if any occurred (after processing all components) + if (componentErrors.Count > 0) + { + // Aggregate flattened error strings to make tests/API assertions simpler + var aggregatedErrors = new System.Collections.Generic.List(); + foreach (var errorObj in componentErrors) + { + try + { + var dataProp = errorObj?.GetType().GetProperty("data"); + var dataVal = dataProp?.GetValue(errorObj); + if (dataVal != null) + { + var errorsProp = dataVal.GetType().GetProperty("errors"); + var errorsEnum = errorsProp?.GetValue(dataVal) as System.Collections.IEnumerable; + if (errorsEnum != null) + { + foreach (var item in errorsEnum) + { + var s = item?.ToString(); + if (!string.IsNullOrEmpty(s)) aggregatedErrors.Add(s); + } + } + } + } + catch { } + } + + return Response.Error( + $"One or more component property operations failed on '{targetGo.name}'.", + new { componentErrors = componentErrors, errors = aggregatedErrors } + ); + } + + if (!modified) + { + // Use the new serializer helper + // return Response.Success( + // $"No modifications applied to GameObject '{targetGo.name}'.", + // GetGameObjectData(targetGo)); + + return Response.Success( + $"No modifications applied to GameObject '{targetGo.name}'.", + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) + ); + } + + EditorUtility.SetDirty(targetGo); // Mark scene as dirty + // Use the new serializer helper + return Response.Success( + $"GameObject '{targetGo.name}' modified successfully.", + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) + ); + // return Response.Success( + // $"GameObject '{targetGo.name}' modified successfully.", + // GetGameObjectData(targetGo)); + + } + + private static object DeleteGameObject(JToken targetToken, string searchMethod) + { + // Find potentially multiple objects if name/tag search is used without find_all=false implicitly + List targets = FindObjectsInternal(targetToken, searchMethod, true); // find_all=true for delete safety + + if (targets.Count == 0) + { + return Response.Error( + $"Target GameObject(s) ('{targetToken}') not found using method '{searchMethod ?? "default"}'." + ); + } + + List deletedObjects = new List(); + foreach (var targetGo in targets) + { + if (targetGo != null) + { + string goName = targetGo.name; + int goId = targetGo.GetInstanceID(); + // Use Undo.DestroyObjectImmediate for undo support + Undo.DestroyObjectImmediate(targetGo); + deletedObjects.Add(new { name = goName, instanceID = goId }); + } + } + + if (deletedObjects.Count > 0) + { + string message = + targets.Count == 1 + ? $"GameObject '{deletedObjects[0].GetType().GetProperty("name").GetValue(deletedObjects[0])}' deleted successfully." + : $"{deletedObjects.Count} GameObjects deleted successfully."; + return Response.Success(message, deletedObjects); + } + else + { + // Should not happen if targets.Count > 0 initially, but defensive check + return Response.Error("Failed to delete target GameObject(s)."); + } + } + + private static object FindGameObjects( + JObject @params, + JToken targetToken, + string searchMethod + ) + { + bool findAll = @params["findAll"]?.ToObject() ?? false; + List foundObjects = FindObjectsInternal( + targetToken, + searchMethod, + findAll, + @params + ); + + if (foundObjects.Count == 0) + { + return Response.Success("No matching GameObjects found.", new List()); + } + + // Use the new serializer helper + //var results = foundObjects.Select(go => GetGameObjectData(go)).ToList(); + var results = foundObjects.Select(go => Helpers.GameObjectSerializer.GetGameObjectData(go)).ToList(); + return Response.Success($"Found {results.Count} GameObject(s).", results); + } + + private static object GetComponentsFromTarget(string target, string searchMethod, bool includeNonPublicSerialized = true) + { + GameObject targetGo = FindObjectInternal(target, searchMethod); + if (targetGo == null) + { + return Response.Error( + $"Target GameObject ('{target}') not found using method '{searchMethod ?? "default"}'." + ); + } + + try + { + // --- Get components, immediately copy to list, and null original array --- + Component[] originalComponents = targetGo.GetComponents(); + List componentsToIterate = new List(originalComponents ?? Array.Empty()); // Copy immediately, handle null case + int componentCount = componentsToIterate.Count; + originalComponents = null; // Null the original reference + // Debug.Log($"[GetComponentsFromTarget] Found {componentCount} components on {targetGo.name}. Copied to list, nulled original. Starting REVERSE for loop..."); + // --- End Copy and Null --- + + var componentData = new List(); + + for (int i = componentCount - 1; i >= 0; i--) // Iterate backwards over the COPY + { + Component c = componentsToIterate[i]; // Use the copy + if (c == null) + { + // Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] Encountered a null component at index {i} on {targetGo.name}. Skipping."); + continue; // Safety check + } + // Debug.Log($"[GetComponentsFromTarget REVERSE for] Processing component: {c.GetType()?.FullName ?? "null"} (ID: {c.GetInstanceID()}) at index {i} on {targetGo.name}"); + try + { + var data = Helpers.GameObjectSerializer.GetComponentData(c, includeNonPublicSerialized); + if (data != null) // Ensure GetComponentData didn't return null + { + componentData.Insert(0, data); // Insert at beginning to maintain original order in final list + } + // else + // { + // Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] GetComponentData returned null for component {c.GetType().FullName} (ID: {c.GetInstanceID()}) on {targetGo.name}. Skipping addition."); + // } + } + catch (Exception ex) + { + Debug.LogError($"[GetComponentsFromTarget REVERSE for] Error processing component {c.GetType().FullName} (ID: {c.GetInstanceID()}) on {targetGo.name}: {ex.Message}\n{ex.StackTrace}"); + // Optionally add placeholder data or just skip + componentData.Insert(0, new JObject( // Insert error marker at beginning + new JProperty("typeName", c.GetType().FullName + " (Serialization Error)"), + new JProperty("instanceID", c.GetInstanceID()), + new JProperty("error", ex.Message) + )); + } + } + // Debug.Log($"[GetComponentsFromTarget] Finished REVERSE for loop."); + + // Cleanup the list we created + componentsToIterate.Clear(); + componentsToIterate = null; + + return Response.Success( + $"Retrieved {componentData.Count} components from '{targetGo.name}'.", + componentData // List was built in original order + ); + } + catch (Exception e) + { + return Response.Error( + $"Error getting components from '{targetGo.name}': {e.Message}" + ); + } + } + + private static object GetSingleComponentFromTarget(string target, string searchMethod, string componentName, bool includeNonPublicSerialized = true) + { + GameObject targetGo = FindObjectInternal(target, searchMethod); + if (targetGo == null) + { + return Response.Error( + $"Target GameObject ('{target}') not found using method '{searchMethod ?? "default"}'." + ); + } + + try + { + // Try to find the component by name + Component targetComponent = targetGo.GetComponent(componentName); + + // If not found directly, try to find by type name (handle namespaces) + if (targetComponent == null) + { + Component[] allComponents = targetGo.GetComponents(); + foreach (Component comp in allComponents) + { + if (comp != null) + { + string typeName = comp.GetType().Name; + string fullTypeName = comp.GetType().FullName; + + if (typeName == componentName || fullTypeName == componentName) + { + targetComponent = comp; + break; + } + } + } + } + + if (targetComponent == null) + { + return Response.Error( + $"Component '{componentName}' not found on GameObject '{targetGo.name}'." + ); + } + + var componentData = Helpers.GameObjectSerializer.GetComponentData(targetComponent, includeNonPublicSerialized); + + if (componentData == null) + { + return Response.Error( + $"Failed to serialize component '{componentName}' on GameObject '{targetGo.name}'." + ); + } + + return Response.Success( + $"Retrieved component '{componentName}' from '{targetGo.name}'.", + componentData + ); + } + catch (Exception e) + { + return Response.Error( + $"Error getting component '{componentName}' from '{targetGo.name}': {e.Message}" + ); + } + } + + private static object AddComponentToTarget( + JObject @params, + JToken targetToken, + string searchMethod + ) + { + GameObject targetGo = FindObjectInternal(targetToken, searchMethod); + if (targetGo == null) + { + return Response.Error( + $"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'." + ); + } + + string typeName = null; + JObject properties = null; + + // Allow adding component specified directly or via componentsToAdd array (take first) + if (@params["componentName"] != null) + { + typeName = @params["componentName"]?.ToString(); + properties = @params["componentProperties"]?[typeName] as JObject; // Check if props are nested under name + } + else if ( + @params["componentsToAdd"] is JArray componentsToAddArray + && componentsToAddArray.Count > 0 + ) + { + var compToken = componentsToAddArray.First; + if (compToken.Type == JTokenType.String) + typeName = compToken.ToString(); + else if (compToken is JObject compObj) + { + typeName = compObj["typeName"]?.ToString(); + properties = compObj["properties"] as JObject; + } + } + + if (string.IsNullOrEmpty(typeName)) + { + return Response.Error( + "Component type name ('componentName' or first element in 'componentsToAdd') is required." + ); + } + + var addResult = AddComponentInternal(targetGo, typeName, properties); + if (addResult != null) + return addResult; // Return error + + EditorUtility.SetDirty(targetGo); + // Use the new serializer helper + return Response.Success( + $"Component '{typeName}' added to '{targetGo.name}'.", + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) + ); // Return updated GO data + } + + private static object RemoveComponentFromTarget( + JObject @params, + JToken targetToken, + string searchMethod + ) + { + GameObject targetGo = FindObjectInternal(targetToken, searchMethod); + if (targetGo == null) + { + return Response.Error( + $"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'." + ); + } + + string typeName = null; + // Allow removing component specified directly or via componentsToRemove array (take first) + if (@params["componentName"] != null) + { + typeName = @params["componentName"]?.ToString(); + } + else if ( + @params["componentsToRemove"] is JArray componentsToRemoveArray + && componentsToRemoveArray.Count > 0 + ) + { + typeName = componentsToRemoveArray.First?.ToString(); + } + + if (string.IsNullOrEmpty(typeName)) + { + return Response.Error( + "Component type name ('componentName' or first element in 'componentsToRemove') is required." + ); + } + + var removeResult = RemoveComponentInternal(targetGo, typeName); + if (removeResult != null) + return removeResult; // Return error + + EditorUtility.SetDirty(targetGo); + // Use the new serializer helper + return Response.Success( + $"Component '{typeName}' removed from '{targetGo.name}'.", + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) + ); + } + + private static object SetComponentPropertyOnTarget( + JObject @params, + JToken targetToken, + string searchMethod + ) + { + GameObject targetGo = FindObjectInternal(targetToken, searchMethod); + if (targetGo == null) + { + return Response.Error( + $"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'." + ); + } + + string compName = @params["componentName"]?.ToString(); + JObject propertiesToSet = null; + + if (!string.IsNullOrEmpty(compName)) + { + // Properties might be directly under componentProperties or nested under the component name + if (@params["componentProperties"] is JObject compProps) + { + propertiesToSet = compProps[compName] as JObject ?? compProps; // Allow flat or nested structure + } + } + else + { + return Response.Error("'componentName' parameter is required."); + } + + if (propertiesToSet == null || !propertiesToSet.HasValues) + { + return Response.Error( + "'componentProperties' dictionary for the specified component is required and cannot be empty." + ); + } + + var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet); + if (setResult != null) + return setResult; // Return error + + EditorUtility.SetDirty(targetGo); + // Use the new serializer helper + return Response.Success( + $"Properties set for component '{compName}' on '{targetGo.name}'.", + Helpers.GameObjectSerializer.GetGameObjectData(targetGo) + ); + } + + // --- Internal Helpers --- + + /// + /// Parses a JArray like [x, y, z] into a Vector3. + /// + private static Vector3? ParseVector3(JArray array) + { + if (array != null && array.Count == 3) + { + try + { + return new Vector3( + array[0].ToObject(), + array[1].ToObject(), + array[2].ToObject() + ); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}"); + } + } + return null; + } + + /// + /// Finds a single GameObject based on token (ID, name, path) and search method. + /// + private static GameObject FindObjectInternal( + JToken targetToken, + string searchMethod, + JObject findParams = null + ) + { + // If find_all is not explicitly false, we still want only one for most single-target operations. + bool findAll = findParams?["findAll"]?.ToObject() ?? false; + // If a specific target ID is given, always find just that one. + if ( + targetToken?.Type == JTokenType.Integer + || (searchMethod == "by_id" && int.TryParse(targetToken?.ToString(), out _)) + ) + { + findAll = false; + } + List results = FindObjectsInternal( + targetToken, + searchMethod, + findAll, + findParams + ); + return results.Count > 0 ? results[0] : null; + } + + /// + /// Core logic for finding GameObjects based on various criteria. + /// + private static List FindObjectsInternal( + JToken targetToken, + string searchMethod, + bool findAll, + JObject findParams = null + ) + { + List results = new List(); + string searchTerm = findParams?["searchTerm"]?.ToString() ?? targetToken?.ToString(); // Use searchTerm if provided, else the target itself + bool searchInChildren = findParams?["searchInChildren"]?.ToObject() ?? false; + bool searchInactive = findParams?["searchInactive"]?.ToObject() ?? false; + + // Default search method if not specified + if (string.IsNullOrEmpty(searchMethod)) + { + if (targetToken?.Type == JTokenType.Integer) + searchMethod = "by_id"; + else if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Contains('/')) + searchMethod = "by_path"; + else + searchMethod = "by_name"; // Default fallback + } + + GameObject rootSearchObject = null; + // If searching in children, find the initial target first + if (searchInChildren && targetToken != null) + { + rootSearchObject = FindObjectInternal(targetToken, "by_id_or_name_or_path"); // Find the root for child search + if (rootSearchObject == null) + { + Debug.LogWarning( + $"[ManageGameObject.Find] Root object '{targetToken}' for child search not found." + ); + return results; // Return empty if root not found + } + } + + switch (searchMethod) + { + case "by_id": + if (int.TryParse(searchTerm, out int instanceId)) + { + // EditorUtility.InstanceIDToObject is slow, iterate manually if possible + // GameObject obj = EditorUtility.InstanceIDToObject(instanceId) as GameObject; + var allObjects = GetAllSceneObjects(searchInactive); // More efficient + GameObject obj = allObjects.FirstOrDefault(go => + go.GetInstanceID() == instanceId + ); + if (obj != null) + results.Add(obj); + } + break; + case "by_name": + var searchPoolName = rootSearchObject + ? rootSearchObject + .GetComponentsInChildren(searchInactive) + .Select(t => t.gameObject) + : GetAllSceneObjects(searchInactive); + results.AddRange(searchPoolName.Where(go => go.name == searchTerm)); + break; + case "by_path": + // Path is relative to scene root or rootSearchObject + Transform foundTransform = rootSearchObject + ? rootSearchObject.transform.Find(searchTerm) + : GameObject.Find(searchTerm)?.transform; + if (foundTransform != null) + results.Add(foundTransform.gameObject); + break; + case "by_tag": + var searchPoolTag = rootSearchObject + ? rootSearchObject + .GetComponentsInChildren(searchInactive) + .Select(t => t.gameObject) + : GetAllSceneObjects(searchInactive); + results.AddRange(searchPoolTag.Where(go => go.CompareTag(searchTerm))); + break; + case "by_layer": + var searchPoolLayer = rootSearchObject + ? rootSearchObject + .GetComponentsInChildren(searchInactive) + .Select(t => t.gameObject) + : GetAllSceneObjects(searchInactive); + if (int.TryParse(searchTerm, out int layerIndex)) + { + results.AddRange(searchPoolLayer.Where(go => go.layer == layerIndex)); + } + else + { + int namedLayer = LayerMask.NameToLayer(searchTerm); + if (namedLayer != -1) + results.AddRange(searchPoolLayer.Where(go => go.layer == namedLayer)); + } + break; + case "by_component": + Type componentType = FindType(searchTerm); + if (componentType != null) + { + // Determine FindObjectsInactive based on the searchInactive flag + FindObjectsInactive findInactive = searchInactive + ? FindObjectsInactive.Include + : FindObjectsInactive.Exclude; + // Replace FindObjectsOfType with FindObjectsByType, specifying the sorting mode and inactive state + var searchPoolComp = rootSearchObject + ? rootSearchObject + .GetComponentsInChildren(componentType, searchInactive) + .Select(c => (c as Component).gameObject) + : UnityEngine + .Object.FindObjectsByType( + componentType, + findInactive, + FindObjectsSortMode.None + ) + .Select(c => (c as Component).gameObject); + results.AddRange(searchPoolComp.Where(go => go != null)); // Ensure GO is valid + } + else + { + Debug.LogWarning( + $"[ManageGameObject.Find] Component type not found: {searchTerm}" + ); + } + break; + case "by_id_or_name_or_path": // Helper method used internally + if (int.TryParse(searchTerm, out int id)) + { + var allObjectsId = GetAllSceneObjects(true); // Search inactive for internal lookup + GameObject objById = allObjectsId.FirstOrDefault(go => + go.GetInstanceID() == id + ); + if (objById != null) + { + results.Add(objById); + break; + } + } + GameObject objByPath = GameObject.Find(searchTerm); + if (objByPath != null) + { + results.Add(objByPath); + break; + } + + var allObjectsName = GetAllSceneObjects(true); + results.AddRange(allObjectsName.Where(go => go.name == searchTerm)); + break; + default: + Debug.LogWarning( + $"[ManageGameObject.Find] Unknown search method: {searchMethod}" + ); + break; + } + + // If only one result is needed, return just the first one found. + if (!findAll && results.Count > 1) + { + return new List { results[0] }; + } + + return results.Distinct().ToList(); // Ensure uniqueness + } + + // Helper to get all scene objects efficiently + private static IEnumerable GetAllSceneObjects(bool includeInactive) + { + // SceneManager.GetActiveScene().GetRootGameObjects() is faster than FindObjectsOfType() + var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects(); + var allObjects = new List(); + foreach (var root in rootObjects) + { + allObjects.AddRange( + root.GetComponentsInChildren(includeInactive) + .Select(t => t.gameObject) + ); + } + return allObjects; + } + + /// + /// Adds a component by type name and optionally sets properties. + /// Returns null on success, or an error response object on failure. + /// + private static object AddComponentInternal( + GameObject targetGo, + string typeName, + JObject properties + ) + { + Type componentType = FindType(typeName); + if (componentType == null) + { + return Response.Error( + $"Component type '{typeName}' not found or is not a valid Component." + ); + } + if (!typeof(Component).IsAssignableFrom(componentType)) + { + return Response.Error($"Type '{typeName}' is not a Component."); + } + + // Prevent adding Transform again + if (componentType == typeof(Transform)) + { + return Response.Error("Cannot add another Transform component."); + } + + // Check for 2D/3D physics component conflicts + bool isAdding2DPhysics = + typeof(Rigidbody2D).IsAssignableFrom(componentType) + || typeof(Collider2D).IsAssignableFrom(componentType); + bool isAdding3DPhysics = + typeof(Rigidbody).IsAssignableFrom(componentType) + || typeof(Collider).IsAssignableFrom(componentType); + + if (isAdding2DPhysics) + { + // Check if the GameObject already has any 3D Rigidbody or Collider + if ( + targetGo.GetComponent() != null + || targetGo.GetComponent() != null + ) + { + return Response.Error( + $"Cannot add 2D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 3D Rigidbody or Collider." + ); + } + } + else if (isAdding3DPhysics) + { + // Check if the GameObject already has any 2D Rigidbody or Collider + if ( + targetGo.GetComponent() != null + || targetGo.GetComponent() != null + ) + { + return Response.Error( + $"Cannot add 3D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 2D Rigidbody or Collider." + ); + } + } + + try + { + // Use Undo.AddComponent for undo support + Component newComponent = Undo.AddComponent(targetGo, componentType); + if (newComponent == null) + { + return Response.Error( + $"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice)." + ); + } + + // Set default values for specific component types + if (newComponent is Light light) + { + // Default newly added lights to directional + light.type = LightType.Directional; + } + + // Set properties if provided + if (properties != null) + { + var setResult = SetComponentPropertiesInternal( + targetGo, + typeName, + properties, + newComponent + ); // Pass the new component instance + if (setResult != null) + { + // If setting properties failed, maybe remove the added component? + Undo.DestroyObjectImmediate(newComponent); + return setResult; // Return the error from setting properties + } + } + + return null; // Success + } + catch (Exception e) + { + return Response.Error( + $"Error adding component '{typeName}' to '{targetGo.name}': {e.Message}" + ); + } + } + + /// + /// Removes a component by type name. + /// Returns null on success, or an error response object on failure. + /// + private static object RemoveComponentInternal(GameObject targetGo, string typeName) + { + Type componentType = FindType(typeName); + if (componentType == null) + { + return Response.Error($"Component type '{typeName}' not found for removal."); + } + + // Prevent removing essential components + if (componentType == typeof(Transform)) + { + return Response.Error("Cannot remove the Transform component."); + } + + Component componentToRemove = targetGo.GetComponent(componentType); + if (componentToRemove == null) + { + return Response.Error( + $"Component '{typeName}' not found on '{targetGo.name}' to remove." + ); + } + + try + { + // Use Undo.DestroyObjectImmediate for undo support + Undo.DestroyObjectImmediate(componentToRemove); + return null; // Success + } + catch (Exception e) + { + return Response.Error( + $"Error removing component '{typeName}' from '{targetGo.name}': {e.Message}" + ); + } + } + + /// + /// Sets properties on a component. + /// Returns null on success, or an error response object on failure. + /// + private static object SetComponentPropertiesInternal( + GameObject targetGo, + string compName, + JObject propertiesToSet, + Component targetComponentInstance = null + ) + { + Component targetComponent = targetComponentInstance; + if (targetComponent == null) + { + if (ComponentResolver.TryResolve(compName, out var compType, out var compError)) + { + targetComponent = targetGo.GetComponent(compType); + } + else + { + targetComponent = targetGo.GetComponent(compName); // fallback to string-based lookup + } + } + if (targetComponent == null) + { + return Response.Error( + $"Component '{compName}' not found on '{targetGo.name}' to set properties." + ); + } + + Undo.RecordObject(targetComponent, "Set Component Properties"); + + var failures = new List(); + foreach (var prop in propertiesToSet.Properties()) + { + string propName = prop.Name; + JToken propValue = prop.Value; + + try + { + bool setResult = SetProperty(targetComponent, propName, propValue); + if (!setResult) + { + var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType()); + var suggestions = ComponentResolver.GetAIPropertySuggestions(propName, availableProperties); + var msg = suggestions.Any() + ? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]" + : $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]"; + Debug.LogWarning($"[ManageGameObject] {msg}"); + failures.Add(msg); + } + } + catch (Exception e) + { + Debug.LogError( + $"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}" + ); + failures.Add($"Error setting '{propName}': {e.Message}"); + } + } + EditorUtility.SetDirty(targetComponent); + return failures.Count == 0 + ? null + : Response.Error($"One or more properties failed on '{compName}'.", new { errors = failures }); + } + + /// + /// Helper to set a property or field via reflection, handling basic types. + /// + private static bool SetProperty(object target, string memberName, JToken value) + { + Type type = target.GetType(); + BindingFlags flags = + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + + // Use shared serializer to avoid per-call allocation + var inputSerializer = InputSerializer; + + try + { + // Handle special case for materials with dot notation (material.property) + // Examples: material.color, sharedMaterial.color, materials[0].color + if (memberName.Contains('.') || memberName.Contains('[')) + { + // Pass the inputSerializer down for nested conversions + return SetNestedProperty(target, memberName, value, inputSerializer); + } + + PropertyInfo propInfo = type.GetProperty(memberName, flags); + if (propInfo != null && propInfo.CanWrite) + { + // Use the inputSerializer for conversion + object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) // Allow setting null + { + propInfo.SetValue(target, convertedValue); + return true; + } + else + { + Debug.LogWarning($"[SetProperty] Conversion failed for property '{memberName}' (Type: {propInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); + } + } + else + { + FieldInfo fieldInfo = type.GetField(memberName, flags); + if (fieldInfo != null) // Check if !IsLiteral? + { + // Use the inputSerializer for conversion + object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) // Allow setting null + { + fieldInfo.SetValue(target, convertedValue); + return true; + } + else + { + Debug.LogWarning($"[SetProperty] Conversion failed for field '{memberName}' (Type: {fieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); + } + } + else + { + // Try NonPublic [SerializeField] fields + var npField = type.GetField(memberName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (npField != null && npField.GetCustomAttribute() != null) + { + object convertedValue = ConvertJTokenToType(value, npField.FieldType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) + { + npField.SetValue(target, convertedValue); + return true; + } + } + } + } + } + catch (Exception ex) + { + Debug.LogError( + $"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}\nToken: {value.ToString(Formatting.None)}" + ); + } + return false; + } + + /// + /// Sets a nested property using dot notation (e.g., "material.color") or array access (e.g., "materials[0]") + /// + // Pass the input serializer for conversions + //Using the serializer helper + private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer) + { + try + { + // Split the path into parts (handling both dot notation and array indexing) + string[] pathParts = SplitPropertyPath(path); + if (pathParts.Length == 0) + return false; + + object currentObject = target; + Type currentType = currentObject.GetType(); + BindingFlags flags = + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + + // Traverse the path until we reach the final property + for (int i = 0; i < pathParts.Length - 1; i++) + { + string part = pathParts[i]; + bool isArray = false; + int arrayIndex = -1; + + // Check if this part contains array indexing + if (part.Contains("[")) + { + int startBracket = part.IndexOf('['); + int endBracket = part.IndexOf(']'); + if (startBracket > 0 && endBracket > startBracket) + { + string indexStr = part.Substring( + startBracket + 1, + endBracket - startBracket - 1 + ); + if (int.TryParse(indexStr, out arrayIndex)) + { + isArray = true; + part = part.Substring(0, startBracket); + } + } + } + // Get the property/field + PropertyInfo propInfo = currentType.GetProperty(part, flags); + FieldInfo fieldInfo = null; + if (propInfo == null) + { + fieldInfo = currentType.GetField(part, flags); + if (fieldInfo == null) + { + Debug.LogWarning( + $"[SetNestedProperty] Could not find property or field '{part}' on type '{currentType.Name}'" + ); + return false; + } + } + + // Get the value + currentObject = + propInfo != null + ? propInfo.GetValue(currentObject) + : fieldInfo.GetValue(currentObject); + //Need to stop if current property is null + if (currentObject == null) + { + Debug.LogWarning( + $"[SetNestedProperty] Property '{part}' is null, cannot access nested properties." + ); + return false; + } + // If this part was an array or list, access the specific index + if (isArray) + { + if (currentObject is Material[]) + { + var materials = currentObject as Material[]; + if (arrayIndex < 0 || arrayIndex >= materials.Length) + { + Debug.LogWarning( + $"[SetNestedProperty] Material index {arrayIndex} out of range (0-{materials.Length - 1})" + ); + return false; + } + currentObject = materials[arrayIndex]; + } + else if (currentObject is System.Collections.IList) + { + var list = currentObject as System.Collections.IList; + if (arrayIndex < 0 || arrayIndex >= list.Count) + { + Debug.LogWarning( + $"[SetNestedProperty] Index {arrayIndex} out of range (0-{list.Count - 1})" + ); + return false; + } + currentObject = list[arrayIndex]; + } + else + { + Debug.LogWarning( + $"[SetNestedProperty] Property '{part}' is not an array or list, cannot access by index." + ); + return false; + } + } + currentType = currentObject.GetType(); + } + + // Set the final property + string finalPart = pathParts[pathParts.Length - 1]; + + // Special handling for Material properties (shader properties) + if (currentObject is Material material && finalPart.StartsWith("_")) + { + // Use the serializer to convert the JToken value first + if (value is JArray jArray) + { + // Try converting to known types that SetColor/SetVector accept + if (jArray.Count == 4) + { + try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } + try { Vector4 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { } + } + else if (jArray.Count == 3) + { + try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } // ToObject handles conversion to Color + } + else if (jArray.Count == 2) + { + try { Vector2 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { } + } + } + else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer) + { + try { material.SetFloat(finalPart, value.ToObject(inputSerializer)); return true; } catch { } + } + else if (value.Type == JTokenType.Boolean) + { + try { material.SetFloat(finalPart, value.ToObject(inputSerializer) ? 1f : 0f); return true; } catch { } + } + else if (value.Type == JTokenType.String) + { + // Try converting to Texture using the serializer/converter + try + { + Texture texture = value.ToObject(inputSerializer); + if (texture != null) + { + material.SetTexture(finalPart, texture); + return true; + } + } + catch { } + } + + Debug.LogWarning( + $"[SetNestedProperty] Unsupported or failed conversion for material property '{finalPart}' from value: {value.ToString(Formatting.None)}" + ); + return false; + } + + // For standard properties (not shader specific) + PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags); + if (finalPropInfo != null && finalPropInfo.CanWrite) + { + // Use the inputSerializer for conversion + object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) + { + finalPropInfo.SetValue(currentObject, convertedValue); + return true; + } + else + { + Debug.LogWarning($"[SetNestedProperty] Final conversion failed for property '{finalPart}' (Type: {finalPropInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); + } + } + else + { + FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags); + if (finalFieldInfo != null) + { + // Use the inputSerializer for conversion + object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer); + if (convertedValue != null || value.Type == JTokenType.Null) + { + finalFieldInfo.SetValue(currentObject, convertedValue); + return true; + } + else + { + Debug.LogWarning($"[SetNestedProperty] Final conversion failed for field '{finalPart}' (Type: {finalFieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); + } + } + else + { + Debug.LogWarning( + $"[SetNestedProperty] Could not find final writable property or field '{finalPart}' on type '{currentType.Name}'" + ); + } + } + } + catch (Exception ex) + { + Debug.LogError( + $"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}\nToken: {value.ToString(Formatting.None)}" + ); + } + + return false; + } + + + /// + /// Split a property path into parts, handling both dot notation and array indexers + /// + private static string[] SplitPropertyPath(string path) + { + // Handle complex paths with both dots and array indexers + List parts = new List(); + int startIndex = 0; + bool inBrackets = false; + + for (int i = 0; i < path.Length; i++) + { + char c = path[i]; + + if (c == '[') + { + inBrackets = true; + } + else if (c == ']') + { + inBrackets = false; + } + else if (c == '.' && !inBrackets) + { + // Found a dot separator outside of brackets + parts.Add(path.Substring(startIndex, i - startIndex)); + startIndex = i + 1; + } + } + if (startIndex < path.Length) + { + parts.Add(path.Substring(startIndex)); + } + return parts.ToArray(); + } + + /// + /// Simple JToken to Type conversion for common Unity types, using JsonSerializer. + /// + // Pass the input serializer + private static object ConvertJTokenToType(JToken token, Type targetType, JsonSerializer inputSerializer) + { + if (token == null || token.Type == JTokenType.Null) + { + if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null) + { + Debug.LogWarning($"Cannot assign null to non-nullable value type {targetType.Name}. Returning default value."); + return Activator.CreateInstance(targetType); + } + return null; + } + + try + { + // Use the provided serializer instance which includes our custom converters + return token.ToObject(targetType, inputSerializer); + } + catch (JsonSerializationException jsonEx) + { + Debug.LogError($"JSON Deserialization Error converting token to {targetType.FullName}: {jsonEx.Message}\nToken: {token.ToString(Formatting.None)}"); + // Optionally re-throw or return null/default + // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + throw; // Re-throw to indicate failure higher up + } + catch (ArgumentException argEx) + { + Debug.LogError($"Argument Error converting token to {targetType.FullName}: {argEx.Message}\nToken: {token.ToString(Formatting.None)}"); + throw; + } + catch (Exception ex) + { + Debug.LogError($"Unexpected error converting token to {targetType.FullName}: {ex}\nToken: {token.ToString(Formatting.None)}"); + throw; + } + // If ToObject succeeded, it would have returned. If it threw, we wouldn't reach here. + // This fallback logic is likely unreachable if ToObject covers all cases or throws on failure. + // Debug.LogWarning($"Conversion failed for token to {targetType.FullName}. Token: {token.ToString(Formatting.None)}"); + // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + } + + // --- ParseJTokenTo... helpers are likely redundant now with the serializer approach --- + // Keep them temporarily for reference or if specific fallback logic is ever needed. + + private static Vector3 ParseJTokenToVector3(JToken token) + { + // ... (implementation - likely replaced by Vector3Converter) ... + // Consider removing these if the serializer handles them reliably. + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z")) + { + return new Vector3(obj["x"].ToObject(), obj["y"].ToObject(), obj["z"].ToObject()); + } + if (token is JArray arr && arr.Count >= 3) + { + return new Vector3(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Vector3 using fallback. Returning Vector3.zero."); + return Vector3.zero; + + } + private static Vector2 ParseJTokenToVector2(JToken token) + { + // ... (implementation - likely replaced by Vector2Converter) ... + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y")) + { + return new Vector2(obj["x"].ToObject(), obj["y"].ToObject()); + } + if (token is JArray arr && arr.Count >= 2) + { + return new Vector2(arr[0].ToObject(), arr[1].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Vector2 using fallback. Returning Vector2.zero."); + return Vector2.zero; + } + private static Quaternion ParseJTokenToQuaternion(JToken token) + { + // ... (implementation - likely replaced by QuaternionConverter) ... + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z") && obj.ContainsKey("w")) + { + return new Quaternion(obj["x"].ToObject(), obj["y"].ToObject(), obj["z"].ToObject(), obj["w"].ToObject()); + } + if (token is JArray arr && arr.Count >= 4) + { + return new Quaternion(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Quaternion using fallback. Returning Quaternion.identity."); + return Quaternion.identity; + } + private static Color ParseJTokenToColor(JToken token) + { + // ... (implementation - likely replaced by ColorConverter) ... + if (token is JObject obj && obj.ContainsKey("r") && obj.ContainsKey("g") && obj.ContainsKey("b") && obj.ContainsKey("a")) + { + return new Color(obj["r"].ToObject(), obj["g"].ToObject(), obj["b"].ToObject(), obj["a"].ToObject()); + } + if (token is JArray arr && arr.Count >= 4) + { + return new Color(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Color using fallback. Returning Color.white."); + return Color.white; + } + private static Rect ParseJTokenToRect(JToken token) + { + // ... (implementation - likely replaced by RectConverter) ... + if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("width") && obj.ContainsKey("height")) + { + return new Rect(obj["x"].ToObject(), obj["y"].ToObject(), obj["width"].ToObject(), obj["height"].ToObject()); + } + if (token is JArray arr && arr.Count >= 4) + { + return new Rect(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject(), arr[3].ToObject()); + } + Debug.LogWarning($"Could not parse JToken '{token}' as Rect using fallback. Returning Rect.zero."); + return Rect.zero; + } + private static Bounds ParseJTokenToBounds(JToken token) + { + // ... (implementation - likely replaced by BoundsConverter) ... + if (token is JObject obj && obj.ContainsKey("center") && obj.ContainsKey("size")) + { + // Requires Vector3 conversion, which should ideally use the serializer too + Vector3 center = ParseJTokenToVector3(obj["center"]); // Or use obj["center"].ToObject(inputSerializer) + Vector3 size = ParseJTokenToVector3(obj["size"]); // Or use obj["size"].ToObject(inputSerializer) + return new Bounds(center, size); + } + // Array fallback for Bounds is less intuitive, maybe remove? + // if (token is JArray arr && arr.Count >= 6) + // { + // return new Bounds(new Vector3(arr[0].ToObject(), arr[1].ToObject(), arr[2].ToObject()), new Vector3(arr[3].ToObject(), arr[4].ToObject(), arr[5].ToObject())); + // } + Debug.LogWarning($"Could not parse JToken '{token}' as Bounds using fallback. Returning new Bounds(Vector3.zero, Vector3.zero)."); + return new Bounds(Vector3.zero, Vector3.zero); + } + // --- End Redundant Parse Helpers --- + + /// + /// Finds a specific UnityEngine.Object based on a find instruction JObject. + /// Primarily used by UnityEngineObjectConverter during deserialization. + /// + // Made public static so UnityEngineObjectConverter can call it. Moved from ConvertJTokenToType. + public static UnityEngine.Object FindObjectByInstruction(JObject instruction, Type targetType) + { + string findTerm = instruction["find"]?.ToString(); + string method = instruction["method"]?.ToString()?.ToLower(); + string componentName = instruction["component"]?.ToString(); // Specific component to get + + if (string.IsNullOrEmpty(findTerm)) + { + Debug.LogWarning("Find instruction missing 'find' term."); + return null; + } + + // Use a flexible default search method if none provided + string searchMethodToUse = string.IsNullOrEmpty(method) ? "by_id_or_name_or_path" : method; + + // If the target is an asset (Material, Texture, ScriptableObject etc.) try AssetDatabase first + if (typeof(Material).IsAssignableFrom(targetType) || + typeof(Texture).IsAssignableFrom(targetType) || + typeof(ScriptableObject).IsAssignableFrom(targetType) || + targetType.FullName.StartsWith("UnityEngine.U2D") || // Sprites etc. + typeof(AudioClip).IsAssignableFrom(targetType) || + typeof(AnimationClip).IsAssignableFrom(targetType) || + typeof(Font).IsAssignableFrom(targetType) || + typeof(Shader).IsAssignableFrom(targetType) || + typeof(ComputeShader).IsAssignableFrom(targetType) || + typeof(GameObject).IsAssignableFrom(targetType) && findTerm.StartsWith("Assets/")) // Prefab check + { + // Try loading directly by path/GUID first + UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(findTerm, targetType); + if (asset != null) return asset; + asset = AssetDatabase.LoadAssetAtPath(findTerm); // Try generic if type specific failed + if (asset != null && targetType.IsAssignableFrom(asset.GetType())) return asset; + + + // If direct path failed, try finding by name/type using FindAssets + string searchFilter = $"t:{targetType.Name} {System.IO.Path.GetFileNameWithoutExtension(findTerm)}"; // Search by type and name + string[] guids = AssetDatabase.FindAssets(searchFilter); + + if (guids.Length == 1) + { + asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0]), targetType); + if (asset != null) return asset; + } + else if (guids.Length > 1) + { + Debug.LogWarning($"[FindObjectByInstruction] Ambiguous asset find: Found {guids.Length} assets matching filter '{searchFilter}'. Provide a full path or unique name."); + // Optionally return the first one? Or null? Returning null is safer. + return null; + } + // If still not found, fall through to scene search (though unlikely for assets) + } + + + // --- Scene Object Search --- + // Find the GameObject using the internal finder + GameObject foundGo = FindObjectInternal(new JValue(findTerm), searchMethodToUse); + + if (foundGo == null) + { + // Don't warn yet, could still be an asset not found above + // Debug.LogWarning($"Could not find GameObject using instruction: {instruction}"); + return null; + } + + // Now, get the target object/component from the found GameObject + if (targetType == typeof(GameObject)) + { + return foundGo; // We were looking for a GameObject + } + else if (typeof(Component).IsAssignableFrom(targetType)) + { + Type componentToGetType = targetType; + if (!string.IsNullOrEmpty(componentName)) + { + Type specificCompType = FindType(componentName); + if (specificCompType != null && typeof(Component).IsAssignableFrom(specificCompType)) + { + componentToGetType = specificCompType; + } + else + { + Debug.LogWarning($"Could not find component type '{componentName}' specified in find instruction. Falling back to target type '{targetType.Name}'."); + } + } + + Component foundComp = foundGo.GetComponent(componentToGetType); + if (foundComp == null) + { + Debug.LogWarning($"Found GameObject '{foundGo.name}' but could not find component of type '{componentToGetType.Name}'."); + } + return foundComp; + } + else + { + Debug.LogWarning($"Find instruction handling not implemented for target type: {targetType.Name}"); + return null; + } + } + + + /// + /// Robust component resolver that avoids Assembly.LoadFrom and works with asmdefs. + /// Searches already-loaded assemblies, prioritizing runtime script assemblies. + /// + private static Type FindType(string typeName) + { + if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error)) + { + return resolvedType; + } + + // Log the resolver error if type wasn't found + if (!string.IsNullOrEmpty(error)) + { + Debug.LogWarning($"[FindType] {error}"); + } + + return null; + } + } + + /// + /// Robust component resolver that avoids Assembly.LoadFrom and supports assembly definitions. + /// Prioritizes runtime (Player) assemblies over Editor assemblies. + /// + internal static class ComponentResolver + { + private static readonly Dictionary CacheByFqn = new(StringComparer.Ordinal); + private static readonly Dictionary CacheByName = new(StringComparer.Ordinal); + + /// + /// Resolve a Component/MonoBehaviour type by short or fully-qualified name. + /// Prefers runtime (Player) script assemblies; falls back to Editor assemblies. + /// Never uses Assembly.LoadFrom. + /// + public static bool TryResolve(string nameOrFullName, out Type type, out string error) + { + error = string.Empty; + type = null!; + + // Handle null/empty input + if (string.IsNullOrWhiteSpace(nameOrFullName)) + { + error = "Component name cannot be null or empty"; + return false; + } + + // 1) Exact cache hits + if (CacheByFqn.TryGetValue(nameOrFullName, out type)) return true; + if (!nameOrFullName.Contains(".") && CacheByName.TryGetValue(nameOrFullName, out type)) return true; + type = Type.GetType(nameOrFullName, throwOnError: false); + if (IsValidComponent(type)) { Cache(type); return true; } + + // 2) Search loaded assemblies (prefer Player assemblies) + var candidates = FindCandidates(nameOrFullName); + if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; } + if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; } + +#if UNITY_EDITOR + // 3) Last resort: Editor-only TypeCache (fast index) + var tc = TypeCache.GetTypesDerivedFrom() + .Where(t => NamesMatch(t, nameOrFullName)); + candidates = PreferPlayer(tc).ToList(); + if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; } + if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; } +#endif + + error = $"Component type '{nameOrFullName}' not found in loaded runtime assemblies. " + + "Use a fully-qualified name (Namespace.TypeName) and ensure the script compiled."; + type = null!; + return false; + } + + private static bool NamesMatch(Type t, string q) => + t.Name.Equals(q, StringComparison.Ordinal) || + (t.FullName?.Equals(q, StringComparison.Ordinal) ?? false); + + private static bool IsValidComponent(Type t) => + t != null && typeof(Component).IsAssignableFrom(t); + + private static void Cache(Type t) + { + if (t.FullName != null) CacheByFqn[t.FullName] = t; + CacheByName[t.Name] = t; + } + + private static List FindCandidates(string query) + { + bool isShort = !query.Contains('.'); + var loaded = AppDomain.CurrentDomain.GetAssemblies(); + +#if UNITY_EDITOR + // Names of Player (runtime) script assemblies (asmdefs + Assembly-CSharp) + var playerAsmNames = new HashSet( + UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name), + StringComparer.Ordinal); + + IEnumerable playerAsms = loaded.Where(a => playerAsmNames.Contains(a.GetName().Name)); + IEnumerable editorAsms = loaded.Except(playerAsms); +#else + IEnumerable playerAsms = loaded; + IEnumerable editorAsms = Array.Empty(); +#endif + static IEnumerable SafeGetTypes(System.Reflection.Assembly a) + { + try { return a.GetTypes(); } + catch (ReflectionTypeLoadException rtle) { return rtle.Types.Where(t => t != null)!; } + } + + Func match = isShort + ? (t => t.Name.Equals(query, StringComparison.Ordinal)) + : (t => t.FullName!.Equals(query, StringComparison.Ordinal)); + + var fromPlayer = playerAsms.SelectMany(SafeGetTypes) + .Where(IsValidComponent) + .Where(match); + var fromEditor = editorAsms.SelectMany(SafeGetTypes) + .Where(IsValidComponent) + .Where(match); + + var list = new List(fromPlayer); + if (list.Count == 0) list.AddRange(fromEditor); + return list; + } + +#if UNITY_EDITOR + private static IEnumerable PreferPlayer(IEnumerable seq) + { + var player = new HashSet( + UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name), + StringComparer.Ordinal); + + return seq.OrderBy(t => player.Contains(t.Assembly.GetName().Name) ? 0 : 1); + } +#endif + + private static string Ambiguity(string query, IEnumerable cands) + { + var lines = cands.Select(t => $"{t.FullName} (assembly {t.Assembly.GetName().Name})"); + return $"Multiple component types matched '{query}':\n - " + string.Join("\n - ", lines) + + "\nProvide a fully qualified type name to disambiguate."; + } + + /// + /// Gets all accessible property and field names from a component type. + /// + public static List GetAllComponentProperties(Type componentType) + { + if (componentType == null) return new List(); + + var properties = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite) + .Select(p => p.Name); + + var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance) + .Where(f => !f.IsInitOnly && !f.IsLiteral) + .Select(f => f.Name); + + // Also include SerializeField private fields (common in Unity) + var serializeFields = componentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(f => f.GetCustomAttribute() != null) + .Select(f => f.Name); + + return properties.Concat(fields).Concat(serializeFields).Distinct().OrderBy(x => x).ToList(); + } + + /// + /// Uses AI to suggest the most likely property matches for a user's input. + /// + public static List GetAIPropertySuggestions(string userInput, List availableProperties) + { + if (string.IsNullOrWhiteSpace(userInput) || !availableProperties.Any()) + return new List(); + + // Simple caching to avoid repeated AI calls for the same input + var cacheKey = $"{userInput.ToLowerInvariant()}:{string.Join(",", availableProperties)}"; + if (PropertySuggestionCache.TryGetValue(cacheKey, out var cached)) + return cached; + + try + { + var prompt = $"A Unity developer is trying to set a component property but used an incorrect name.\n\n" + + $"User requested: \"{userInput}\"\n" + + $"Available properties: [{string.Join(", ", availableProperties)}]\n\n" + + $"Find 1-3 most likely matches considering:\n" + + $"- Unity Inspector display names vs actual field names (e.g., \"Max Reach Distance\" → \"maxReachDistance\")\n" + + $"- camelCase vs PascalCase vs spaces\n" + + $"- Similar meaning/semantics\n" + + $"- Common Unity naming patterns\n\n" + + $"Return ONLY the matching property names, comma-separated, no quotes or explanation.\n" + + $"If confidence is low (<70%), return empty string.\n\n" + + $"Examples:\n" + + $"- \"Max Reach Distance\" → \"maxReachDistance\"\n" + + $"- \"Health Points\" → \"healthPoints, hp\"\n" + + $"- \"Move Speed\" → \"moveSpeed, movementSpeed\""; + + // For now, we'll use a simple rule-based approach that mimics AI behavior + // This can be replaced with actual AI calls later + var suggestions = GetRuleBasedSuggestions(userInput, availableProperties); + + PropertySuggestionCache[cacheKey] = suggestions; + return suggestions; + } + catch (Exception ex) + { + Debug.LogWarning($"[AI Property Matching] Error getting suggestions for '{userInput}': {ex.Message}"); + return new List(); + } + } + + private static readonly Dictionary> PropertySuggestionCache = new(); + + /// + /// Rule-based suggestions that mimic AI behavior for property matching. + /// This provides immediate value while we could add real AI integration later. + /// + private static List GetRuleBasedSuggestions(string userInput, List availableProperties) + { + var suggestions = new List(); + var cleanedInput = userInput.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", ""); + + foreach (var property in availableProperties) + { + var cleanedProperty = property.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", ""); + + // Exact match after cleaning + if (cleanedProperty == cleanedInput) + { + suggestions.Add(property); + continue; + } + + // Check if property contains all words from input + var inputWords = userInput.ToLowerInvariant().Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries); + if (inputWords.All(word => cleanedProperty.Contains(word.ToLowerInvariant()))) + { + suggestions.Add(property); + continue; + } + + // Levenshtein distance for close matches + if (LevenshteinDistance(cleanedInput, cleanedProperty) <= Math.Max(2, cleanedInput.Length / 4)) + { + suggestions.Add(property); + } + } + + // Prioritize exact matches, then by similarity + return suggestions.OrderBy(s => LevenshteinDistance(cleanedInput, s.ToLowerInvariant().Replace(" ", ""))) + .Take(3) + .ToList(); + } + + /// + /// Calculates Levenshtein distance between two strings for similarity matching. + /// + private static int LevenshteinDistance(string s1, string s2) + { + if (string.IsNullOrEmpty(s1)) return s2?.Length ?? 0; + if (string.IsNullOrEmpty(s2)) return s1.Length; + + var matrix = new int[s1.Length + 1, s2.Length + 1]; + + for (int i = 0; i <= s1.Length; i++) matrix[i, 0] = i; + for (int j = 0; j <= s2.Length; j++) matrix[0, j] = j; + + for (int i = 1; i <= s1.Length; i++) + { + for (int j = 1; j <= s2.Length; j++) + { + int cost = (s2[j - 1] == s1[i - 1]) ? 0 : 1; + matrix[i, j] = Math.Min(Math.Min( + matrix[i - 1, j] + 1, // deletion + matrix[i, j - 1] + 1), // insertion + matrix[i - 1, j - 1] + cost); // substitution + } + } + + return matrix[s1.Length, s2.Length]; + } + + // Removed duplicate ParseVector3 - using the one at line 1114 + + // Removed GetGameObjectData, GetComponentData, and related private helpers/caching/serializer setup. + // They are now in Helpers.GameObjectSerializer + } +} diff --git a/MCPForUnity/Editor/Tools/ManageGameObject.cs.meta b/MCPForUnity/Editor/Tools/ManageGameObject.cs.meta new file mode 100644 index 00000000..5093c861 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageGameObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7641d7388f0f6634b9d83d34de87b2ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs new file mode 100644 index 00000000..6a310d02 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -0,0 +1,475 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using MCPForUnity.Editor.Helpers; // For Response class + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Handles scene management operations like loading, saving, creating, and querying hierarchy. + /// + [McpForUnityTool("manage_scene")] + public static class ManageScene + { + private sealed class SceneCommand + { + public string action { get; set; } = string.Empty; + public string name { get; set; } = string.Empty; + public string path { get; set; } = string.Empty; + public int? buildIndex { get; set; } + } + + private static SceneCommand ToSceneCommand(JObject p) + { + if (p == null) return new SceneCommand(); + int? BI(JToken t) + { + if (t == null || t.Type == JTokenType.Null) return null; + var s = t.ToString().Trim(); + if (s.Length == 0) return null; + if (int.TryParse(s, out var i)) return i; + if (double.TryParse(s, out var d)) return (int)d; + return t.Type == JTokenType.Integer ? t.Value() : (int?)null; + } + return new SceneCommand + { + action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(), + name = p["name"]?.ToString() ?? string.Empty, + path = p["path"]?.ToString() ?? string.Empty, + buildIndex = BI(p["buildIndex"] ?? p["build_index"]) + }; + } + + /// + /// Main handler for scene management actions. + /// + public static object HandleCommand(JObject @params) + { + try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { } + var cmd = ToSceneCommand(@params); + string action = cmd.action; + string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name; + string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/ + int? buildIndex = cmd.buildIndex; + // bool loadAdditive = @params["loadAdditive"]?.ToObject() ?? false; // Example for future extension + + // Ensure path is relative to Assets/, removing any leading "Assets/" + string relativeDir = path ?? string.Empty; + if (!string.IsNullOrEmpty(relativeDir)) + { + relativeDir = relativeDir.Replace('\\', '/').Trim('/'); + if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/'); + } + } + + // Apply default *after* sanitizing, using the original path variable for the check + if (string.IsNullOrEmpty(path) && action == "create") // Check original path for emptiness + { + relativeDir = "Scenes"; // Default relative directory + } + + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + + string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}.unity"; + // Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName + string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets) + string fullPath = string.IsNullOrEmpty(sceneFileName) + ? null + : Path.Combine(fullPathDir, sceneFileName); + // Ensure relativePath always starts with "Assets/" and uses forward slashes + string relativePath = string.IsNullOrEmpty(sceneFileName) + ? null + : Path.Combine("Assets", relativeDir, sceneFileName).Replace('\\', '/'); + + // Ensure directory exists for 'create' + if (action == "create" && !string.IsNullOrEmpty(fullPathDir)) + { + try + { + Directory.CreateDirectory(fullPathDir); + } + catch (Exception e) + { + return Response.Error( + $"Could not create directory '{fullPathDir}': {e.Message}" + ); + } + } + + // Route action + try { McpLog.Info($"[ManageScene] Route action='{action}' name='{name}' path='{path}' buildIndex={(buildIndex.HasValue ? buildIndex.Value.ToString() : "null")}", always: false); } catch { } + switch (action) + { + case "create": + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(relativePath)) + return Response.Error( + "'name' and 'path' parameters are required for 'create' action." + ); + return CreateScene(fullPath, relativePath); + case "load": + // Loading can be done by path/name or build index + if (!string.IsNullOrEmpty(relativePath)) + return LoadScene(relativePath); + else if (buildIndex.HasValue) + return LoadScene(buildIndex.Value); + else + return Response.Error( + "Either 'name'/'path' or 'buildIndex' must be provided for 'load' action." + ); + case "save": + // Save current scene, optionally to a new path + return SaveScene(fullPath, relativePath); + case "get_hierarchy": + try { McpLog.Info("[ManageScene] get_hierarchy: entering", always: false); } catch { } + var gh = GetSceneHierarchy(); + try { McpLog.Info("[ManageScene] get_hierarchy: exiting", always: false); } catch { } + return gh; + case "get_active": + try { McpLog.Info("[ManageScene] get_active: entering", always: false); } catch { } + var ga = GetActiveSceneInfo(); + try { McpLog.Info("[ManageScene] get_active: exiting", always: false); } catch { } + return ga; + case "get_build_settings": + return GetBuildSettingsScenes(); + // Add cases for modifying build settings, additive loading, unloading etc. + default: + return Response.Error( + $"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings." + ); + } + } + + private static object CreateScene(string fullPath, string relativePath) + { + if (File.Exists(fullPath)) + { + return Response.Error($"Scene already exists at '{relativePath}'."); + } + + try + { + // Create a new empty scene + Scene newScene = EditorSceneManager.NewScene( + NewSceneSetup.EmptyScene, + NewSceneMode.Single + ); + // Save it to the specified path + bool saved = EditorSceneManager.SaveScene(newScene, relativePath); + + if (saved) + { + AssetDatabase.Refresh(); // Ensure Unity sees the new scene file + return Response.Success( + $"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.", + new { path = relativePath } + ); + } + else + { + // If SaveScene fails, it might leave an untitled scene open. + // Optionally try to close it, but be cautious. + return Response.Error($"Failed to save new scene to '{relativePath}'."); + } + } + catch (Exception e) + { + return Response.Error($"Error creating scene '{relativePath}': {e.Message}"); + } + } + + private static object LoadScene(string relativePath) + { + if ( + !File.Exists( + Path.Combine( + Application.dataPath.Substring( + 0, + Application.dataPath.Length - "Assets".Length + ), + relativePath + ) + ) + ) + { + return Response.Error($"Scene file not found at '{relativePath}'."); + } + + // Check for unsaved changes in the current scene + if (EditorSceneManager.GetActiveScene().isDirty) + { + // Optionally prompt the user or save automatically before loading + return Response.Error( + "Current scene has unsaved changes. Please save or discard changes before loading a new scene." + ); + // Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); + // if (!saveOK) return Response.Error("Load cancelled by user."); + } + + try + { + EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single); + return Response.Success( + $"Scene '{relativePath}' loaded successfully.", + new + { + path = relativePath, + name = Path.GetFileNameWithoutExtension(relativePath), + } + ); + } + catch (Exception e) + { + return Response.Error($"Error loading scene '{relativePath}': {e.Message}"); + } + } + + private static object LoadScene(int buildIndex) + { + if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings) + { + return Response.Error( + $"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}." + ); + } + + // Check for unsaved changes + if (EditorSceneManager.GetActiveScene().isDirty) + { + return Response.Error( + "Current scene has unsaved changes. Please save or discard changes before loading a new scene." + ); + } + + try + { + string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex); + EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); + return Response.Success( + $"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.", + new + { + path = scenePath, + name = Path.GetFileNameWithoutExtension(scenePath), + buildIndex = buildIndex, + } + ); + } + catch (Exception e) + { + return Response.Error( + $"Error loading scene with build index {buildIndex}: {e.Message}" + ); + } + } + + private static object SaveScene(string fullPath, string relativePath) + { + try + { + Scene currentScene = EditorSceneManager.GetActiveScene(); + if (!currentScene.IsValid()) + { + return Response.Error("No valid scene is currently active to save."); + } + + bool saved; + string finalPath = currentScene.path; // Path where it was last saved or will be saved + + if (!string.IsNullOrEmpty(relativePath) && currentScene.path != relativePath) + { + // Save As... + // Ensure directory exists + string dir = Path.GetDirectoryName(fullPath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + saved = EditorSceneManager.SaveScene(currentScene, relativePath); + finalPath = relativePath; + } + else + { + // Save (overwrite existing or save untitled) + if (string.IsNullOrEmpty(currentScene.path)) + { + // Scene is untitled, needs a path + return Response.Error( + "Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality." + ); + } + saved = EditorSceneManager.SaveScene(currentScene); + } + + if (saved) + { + AssetDatabase.Refresh(); + return Response.Success( + $"Scene '{currentScene.name}' saved successfully to '{finalPath}'.", + new { path = finalPath, name = currentScene.name } + ); + } + else + { + return Response.Error($"Failed to save scene '{currentScene.name}'."); + } + } + catch (Exception e) + { + return Response.Error($"Error saving scene: {e.Message}"); + } + } + + private static object GetActiveSceneInfo() + { + try + { + try { McpLog.Info("[ManageScene] get_active: querying EditorSceneManager.GetActiveScene", always: false); } catch { } + Scene activeScene = EditorSceneManager.GetActiveScene(); + try { McpLog.Info($"[ManageScene] get_active: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { } + if (!activeScene.IsValid()) + { + return Response.Error("No active scene found."); + } + + var sceneInfo = new + { + name = activeScene.name, + path = activeScene.path, + buildIndex = activeScene.buildIndex, // -1 if not in build settings + isDirty = activeScene.isDirty, + isLoaded = activeScene.isLoaded, + rootCount = activeScene.rootCount, + }; + + return Response.Success("Retrieved active scene information.", sceneInfo); + } + catch (Exception e) + { + try { McpLog.Error($"[ManageScene] get_active: exception {e.Message}"); } catch { } + return Response.Error($"Error getting active scene info: {e.Message}"); + } + } + + private static object GetBuildSettingsScenes() + { + try + { + var scenes = new List(); + for (int i = 0; i < EditorBuildSettings.scenes.Length; i++) + { + var scene = EditorBuildSettings.scenes[i]; + scenes.Add( + new + { + path = scene.path, + guid = scene.guid.ToString(), + enabled = scene.enabled, + buildIndex = i, // Actual build index considering only enabled scenes might differ + } + ); + } + return Response.Success("Retrieved scenes from Build Settings.", scenes); + } + catch (Exception e) + { + return Response.Error($"Error getting scenes from Build Settings: {e.Message}"); + } + } + + private static object GetSceneHierarchy() + { + try + { + try { McpLog.Info("[ManageScene] get_hierarchy: querying EditorSceneManager.GetActiveScene", always: false); } catch { } + Scene activeScene = EditorSceneManager.GetActiveScene(); + try { McpLog.Info($"[ManageScene] get_hierarchy: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { } + if (!activeScene.IsValid() || !activeScene.isLoaded) + { + return Response.Error( + "No valid and loaded scene is active to get hierarchy from." + ); + } + + try { McpLog.Info("[ManageScene] get_hierarchy: fetching root objects", always: false); } catch { } + GameObject[] rootObjects = activeScene.GetRootGameObjects(); + try { McpLog.Info($"[ManageScene] get_hierarchy: rootCount={rootObjects?.Length ?? 0}", always: false); } catch { } + var hierarchy = rootObjects.Select(go => GetGameObjectDataRecursive(go)).ToList(); + + var resp = Response.Success( + $"Retrieved hierarchy for scene '{activeScene.name}'.", + hierarchy + ); + try { McpLog.Info("[ManageScene] get_hierarchy: success", always: false); } catch { } + return resp; + } + catch (Exception e) + { + try { McpLog.Error($"[ManageScene] get_hierarchy: exception {e.Message}"); } catch { } + return Response.Error($"Error getting scene hierarchy: {e.Message}"); + } + } + + /// + /// Recursively builds a data representation of a GameObject and its children. + /// + private static object GetGameObjectDataRecursive(GameObject go) + { + if (go == null) + return null; + + var childrenData = new List(); + foreach (Transform child in go.transform) + { + childrenData.Add(GetGameObjectDataRecursive(child.gameObject)); + } + + var gameObjectData = new Dictionary + { + { "name", go.name }, + { "activeSelf", go.activeSelf }, + { "activeInHierarchy", go.activeInHierarchy }, + { "tag", go.tag }, + { "layer", go.layer }, + { "isStatic", go.isStatic }, + { "instanceID", go.GetInstanceID() }, // Useful unique identifier + { + "transform", + new + { + position = new + { + x = go.transform.localPosition.x, + y = go.transform.localPosition.y, + z = go.transform.localPosition.z, + }, + rotation = new + { + x = go.transform.localRotation.eulerAngles.x, + y = go.transform.localRotation.eulerAngles.y, + z = go.transform.localRotation.eulerAngles.z, + }, // Euler for simplicity + scale = new + { + x = go.transform.localScale.x, + y = go.transform.localScale.y, + z = go.transform.localScale.z, + }, + } + }, + { "children", childrenData }, + }; + + return gameObjectData; + } + } +} diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs.meta b/MCPForUnity/Editor/Tools/ManageScene.cs.meta new file mode 100644 index 00000000..532618aa --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageScene.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b6ddda47f4077e74fbb5092388cefcc2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs new file mode 100644 index 00000000..2d970486 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -0,0 +1,2661 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Helpers; +using System.Threading; +using System.Security.Cryptography; + +#if USE_ROSLYN +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Formatting; +#endif + +#if UNITY_EDITOR +using UnityEditor.Compilation; +#endif + + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Handles CRUD operations for C# scripts within the Unity project. + /// + /// ROSLYN INSTALLATION GUIDE: + /// To enable advanced syntax validation with Roslyn compiler services: + /// + /// 1. Install Microsoft.CodeAnalysis.CSharp NuGet package: + /// - Open Package Manager in Unity + /// - Follow the instruction on https://github.com/GlitchEnzo/NuGetForUnity + /// + /// 2. Open NuGet Package Manager and Install Microsoft.CodeAnalysis.CSharp: + /// + /// 3. Alternative: Manual DLL installation: + /// - Download Microsoft.CodeAnalysis.CSharp.dll and dependencies + /// - Place in Assets/Plugins/ folder + /// - Ensure .NET compatibility settings are correct + /// + /// 4. Define USE_ROSLYN symbol: + /// - Go to Player Settings > Scripting Define Symbols + /// - Add "USE_ROSLYN" to enable Roslyn-based validation + /// + /// 5. Restart Unity after installation + /// + /// Note: Without Roslyn, the system falls back to basic structural validation. + /// Roslyn provides full C# compiler diagnostics with line numbers and detailed error messages. + /// + [McpForUnityTool("manage_script")] + public static class ManageScript + { + /// + /// Resolves a directory under Assets/, preventing traversal and escaping. + /// Returns fullPathDir on disk and canonical 'Assets/...' relative path. + /// + private static bool TryResolveUnderAssets(string relDir, out string fullPathDir, out string relPathSafe) + { + string assets = Application.dataPath.Replace('\\', '/'); + + // Normalize caller path: allow both "Scripts/..." and "Assets/Scripts/..." + string rel = (relDir ?? "Scripts").Replace('\\', '/').Trim(); + if (string.IsNullOrEmpty(rel)) rel = "Scripts"; + if (rel.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) rel = rel.Substring(7); + rel = rel.TrimStart('/'); + + string targetDir = Path.Combine(assets, rel).Replace('\\', '/'); + string full = Path.GetFullPath(targetDir).Replace('\\', '/'); + + bool underAssets = full.StartsWith(assets + "/", StringComparison.OrdinalIgnoreCase) + || string.Equals(full, assets, StringComparison.OrdinalIgnoreCase); + if (!underAssets) + { + fullPathDir = null; + relPathSafe = null; + return false; + } + + // Best-effort symlink guard: if the directory OR ANY ANCESTOR (up to Assets/) is a reparse point/symlink, reject + try + { + var di = new DirectoryInfo(full); + while (di != null) + { + if (di.Exists && (di.Attributes & FileAttributes.ReparsePoint) != 0) + { + fullPathDir = null; + relPathSafe = null; + return false; + } + var atAssets = string.Equals( + di.FullName.Replace('\\', '/'), + assets, + StringComparison.OrdinalIgnoreCase + ); + if (atAssets) break; + di = di.Parent; + } + } + catch { /* best effort; proceed */ } + + fullPathDir = full; + string tail = full.Length > assets.Length ? full.Substring(assets.Length).TrimStart('/') : string.Empty; + relPathSafe = ("Assets/" + tail).TrimEnd('/'); + return true; + } + /// + /// Main handler for script management actions. + /// + public static object HandleCommand(JObject @params) + { + // Handle null parameters + if (@params == null) + { + return Response.Error("invalid_params", "Parameters cannot be null."); + } + + // Extract parameters + string action = @params["action"]?.ToString()?.ToLower(); + string name = @params["name"]?.ToString(); + string path = @params["path"]?.ToString(); // Relative to Assets/ + string contents = null; + + // Check if we have base64 encoded contents + bool contentsEncoded = @params["contentsEncoded"]?.ToObject() ?? false; + if (contentsEncoded && @params["encodedContents"] != null) + { + try + { + contents = DecodeBase64(@params["encodedContents"].ToString()); + } + catch (Exception e) + { + return Response.Error($"Failed to decode script contents: {e.Message}"); + } + } + else + { + contents = @params["contents"]?.ToString(); + } + + string scriptType = @params["scriptType"]?.ToString(); // For templates/validation + string namespaceName = @params["namespace"]?.ToString(); // For organizing code + + // Validate required parameters + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + if (string.IsNullOrEmpty(name)) + { + return Response.Error("Name parameter is required."); + } + // Basic name validation (alphanumeric, underscores, cannot start with number) + if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2))) + { + return Response.Error( + $"Invalid script name: '{name}'. Use only letters, numbers, underscores, and don't start with a number." + ); + } + + // Resolve and harden target directory under Assets/ + if (!TryResolveUnderAssets(path, out string fullPathDir, out string relPathSafeDir)) + { + return Response.Error($"Invalid path. Target directory must be within 'Assets/'. Provided: '{(path ?? "(null)")}'"); + } + + // Construct file paths + string scriptFileName = $"{name}.cs"; + string fullPath = Path.Combine(fullPathDir, scriptFileName); + string relativePath = Path.Combine(relPathSafeDir, scriptFileName).Replace('\\', '/'); + + // Ensure the target directory exists for create/update + if (action == "create" || action == "update") + { + try + { + Directory.CreateDirectory(fullPathDir); + } + catch (Exception e) + { + return Response.Error( + $"Could not create directory '{fullPathDir}': {e.Message}" + ); + } + } + + // Route to specific action handlers + switch (action) + { + case "create": + return CreateScript( + fullPath, + relativePath, + name, + contents, + scriptType, + namespaceName + ); + case "read": + McpLog.Warn("manage_script.read is deprecated; prefer resources/read. Serving read for backward compatibility."); + return ReadScript(fullPath, relativePath); + case "update": + McpLog.Warn("manage_script.update is deprecated; prefer apply_text_edits. Serving update for backward compatibility."); + return UpdateScript(fullPath, relativePath, name, contents); + case "delete": + return DeleteScript(fullPath, relativePath); + case "apply_text_edits": + { + var textEdits = @params["edits"] as JArray; + string precondition = @params["precondition_sha256"]?.ToString(); + // Respect optional options + string refreshOpt = @params["options"]?["refresh"]?.ToString()?.ToLowerInvariant(); + string validateOpt = @params["options"]?["validate"]?.ToString()?.ToLowerInvariant(); + return ApplyTextEdits(fullPath, relativePath, name, textEdits, precondition, refreshOpt, validateOpt); + } + case "validate": + { + string level = @params["level"]?.ToString()?.ToLowerInvariant() ?? "standard"; + var chosen = level switch + { + "basic" => ValidationLevel.Basic, + "standard" => ValidationLevel.Standard, + "strict" => ValidationLevel.Strict, + "comprehensive" => ValidationLevel.Comprehensive, + _ => ValidationLevel.Standard + }; + string fileText; + try { fileText = File.ReadAllText(fullPath); } + catch (Exception ex) { return Response.Error($"Failed to read script: {ex.Message}"); } + + bool ok = ValidateScriptSyntax(fileText, chosen, out string[] diagsRaw); + var diags = (diagsRaw ?? Array.Empty()).Select(s => + { + var m = Regex.Match( + s, + @"^(ERROR|WARNING|INFO): (.*?)(?: \(Line (\d+)\))?$", + RegexOptions.CultureInvariant | RegexOptions.Multiline, + TimeSpan.FromMilliseconds(250) + ); + string severity = m.Success ? m.Groups[1].Value.ToLowerInvariant() : "info"; + string message = m.Success ? m.Groups[2].Value : s; + int lineNum = m.Success && int.TryParse(m.Groups[3].Value, out var l) ? l : 0; + return new { line = lineNum, col = 0, severity, message }; + }).ToArray(); + + var result = new { diagnostics = diags }; + return ok ? Response.Success("Validation completed.", result) + : Response.Error("Validation failed.", result); + } + case "edit": + Debug.LogWarning("manage_script.edit is deprecated; prefer apply_text_edits. Serving structured edit for backward compatibility."); + var structEdits = @params["edits"] as JArray; + var options = @params["options"] as JObject; + return EditScript(fullPath, relativePath, name, structEdits, options); + case "get_sha": + { + try + { + if (!File.Exists(fullPath)) + return Response.Error($"Script not found at '{relativePath}'."); + + string text = File.ReadAllText(fullPath); + string sha = ComputeSha256(text); + var fi = new FileInfo(fullPath); + long lengthBytes; + try { lengthBytes = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetByteCount(text); } + catch { lengthBytes = fi.Exists ? fi.Length : 0; } + var data = new + { + uri = $"unity://path/{relativePath}", + path = relativePath, + sha256 = sha, + lengthBytes, + lastModifiedUtc = fi.Exists ? fi.LastWriteTimeUtc.ToString("o") : string.Empty + }; + return Response.Success($"SHA computed for '{relativePath}'.", data); + } + catch (Exception ex) + { + return Response.Error($"Failed to compute SHA: {ex.Message}"); + } + } + default: + return Response.Error( + $"Unknown action: '{action}'. Valid actions are: create, delete, apply_text_edits, validate, read (deprecated), update (deprecated), edit (deprecated)." + ); + } + } + + /// + /// Decode base64 string to normal text + /// + private static string DecodeBase64(string encoded) + { + byte[] data = Convert.FromBase64String(encoded); + return System.Text.Encoding.UTF8.GetString(data); + } + + /// + /// Encode text to base64 string + /// + private static string EncodeBase64(string text) + { + byte[] data = System.Text.Encoding.UTF8.GetBytes(text); + return Convert.ToBase64String(data); + } + + private static object CreateScript( + string fullPath, + string relativePath, + string name, + string contents, + string scriptType, + string namespaceName + ) + { + // Check if script already exists + if (File.Exists(fullPath)) + { + return Response.Error( + $"Script already exists at '{relativePath}'. Use 'update' action to modify." + ); + } + + // Generate default content if none provided + if (string.IsNullOrEmpty(contents)) + { + contents = GenerateDefaultScriptContent(name, scriptType, namespaceName); + } + + // Validate syntax with detailed error reporting using GUI setting + ValidationLevel validationLevel = GetValidationLevelFromGUI(); + bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors); + if (!isValid) + { + return Response.Error("validation_failed", new { status = "validation_failed", diagnostics = validationErrors ?? Array.Empty() }); + } + else if (validationErrors != null && validationErrors.Length > 0) + { + // Log warnings but don't block creation + Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors)); + } + + try + { + // Atomic create without BOM; schedule refresh after reply + var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + var tmp = fullPath + ".tmp"; + File.WriteAllText(tmp, contents, enc); + try + { + File.Move(tmp, fullPath); + } + catch (IOException) + { + File.Copy(tmp, fullPath, overwrite: true); + try { File.Delete(tmp); } catch { } + } + + var uri = $"unity://path/{relativePath}"; + var ok = Response.Success( + $"Script '{name}.cs' created successfully at '{relativePath}'.", + new { uri, scheduledRefresh = false } + ); + + ManageScriptRefreshHelpers.ImportAndRequestCompile(relativePath); + + return ok; + } + catch (Exception e) + { + return Response.Error($"Failed to create script '{relativePath}': {e.Message}"); + } + } + + private static object ReadScript(string fullPath, string relativePath) + { + if (!File.Exists(fullPath)) + { + return Response.Error($"Script not found at '{relativePath}'."); + } + + try + { + string contents = File.ReadAllText(fullPath); + + // Return both normal and encoded contents for larger files + bool isLarge = contents.Length > 10000; // If content is large, include encoded version + var uri = $"unity://path/{relativePath}"; + var responseData = new + { + uri, + path = relativePath, + contents = contents, + // For large files, also include base64-encoded version + encodedContents = isLarge ? EncodeBase64(contents) : null, + contentsEncoded = isLarge, + }; + + return Response.Success( + $"Script '{Path.GetFileName(relativePath)}' read successfully.", + responseData + ); + } + catch (Exception e) + { + return Response.Error($"Failed to read script '{relativePath}': {e.Message}"); + } + } + + private static object UpdateScript( + string fullPath, + string relativePath, + string name, + string contents + ) + { + if (!File.Exists(fullPath)) + { + return Response.Error( + $"Script not found at '{relativePath}'. Use 'create' action to add a new script." + ); + } + if (string.IsNullOrEmpty(contents)) + { + return Response.Error("Content is required for the 'update' action."); + } + + // Validate syntax with detailed error reporting using GUI setting + ValidationLevel validationLevel = GetValidationLevelFromGUI(); + bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors); + if (!isValid) + { + return Response.Error("validation_failed", new { status = "validation_failed", diagnostics = validationErrors ?? Array.Empty() }); + } + else if (validationErrors != null && validationErrors.Length > 0) + { + // Log warnings but don't block update + Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors)); + } + + try + { + // Safe write with atomic replace when available, without BOM + var encoding = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + string tempPath = fullPath + ".tmp"; + File.WriteAllText(tempPath, contents, encoding); + + string backupPath = fullPath + ".bak"; + try + { + File.Replace(tempPath, fullPath, backupPath); + try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { } + } + catch (PlatformNotSupportedException) + { + File.Copy(tempPath, fullPath, true); + try { File.Delete(tempPath); } catch { } + try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { } + } + catch (IOException) + { + File.Copy(tempPath, fullPath, true); + try { File.Delete(tempPath); } catch { } + try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { } + } + + // Prepare success response BEFORE any operation that can trigger a domain reload + var uri = $"unity://path/{relativePath}"; + var ok = Response.Success( + $"Script '{name}.cs' updated successfully at '{relativePath}'.", + new { uri, path = relativePath, scheduledRefresh = true } + ); + + // Schedule a debounced import/compile on next editor tick to avoid stalling the reply + ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath); + + return ok; + } + catch (Exception e) + { + return Response.Error($"Failed to update script '{relativePath}': {e.Message}"); + } + } + + /// + /// Apply simple text edits specified by line/column ranges. Applies transactionally and validates result. + /// + private const int MaxEditPayloadBytes = 64 * 1024; + + private static object ApplyTextEdits( + string fullPath, + string relativePath, + string name, + JArray edits, + string preconditionSha256, + string refreshModeFromCaller = null, + string validateMode = null) + { + if (!File.Exists(fullPath)) + return Response.Error($"Script not found at '{relativePath}'."); + // Refuse edits if the target or any ancestor is a symlink + try + { + var di = new DirectoryInfo(Path.GetDirectoryName(fullPath) ?? ""); + while (di != null && !string.Equals(di.FullName.Replace('\\', '/'), Application.dataPath.Replace('\\', '/'), StringComparison.OrdinalIgnoreCase)) + { + if (di.Exists && (di.Attributes & FileAttributes.ReparsePoint) != 0) + return Response.Error("Refusing to edit a symlinked script path."); + di = di.Parent; + } + } + catch + { + // If checking attributes fails, proceed without the symlink guard + } + if (edits == null || edits.Count == 0) + return Response.Error("No edits provided."); + + string original; + try { original = File.ReadAllText(fullPath); } + catch (Exception ex) { return Response.Error($"Failed to read script: {ex.Message}"); } + + // Require precondition to avoid drift on large files + string currentSha = ComputeSha256(original); + if (string.IsNullOrEmpty(preconditionSha256)) + return Response.Error("precondition_required", new { status = "precondition_required", current_sha256 = currentSha }); + if (!preconditionSha256.Equals(currentSha, StringComparison.OrdinalIgnoreCase)) + return Response.Error("stale_file", new { status = "stale_file", expected_sha256 = preconditionSha256, current_sha256 = currentSha }); + + // Convert edits to absolute index ranges + var spans = new List<(int start, int end, string text)>(); + long totalBytes = 0; + foreach (var e in edits) + { + try + { + int sl = Math.Max(1, e.Value("startLine")); + int sc = Math.Max(1, e.Value("startCol")); + int el = Math.Max(1, e.Value("endLine")); + int ec = Math.Max(1, e.Value("endCol")); + string newText = e.Value("newText") ?? string.Empty; + + if (!TryIndexFromLineCol(original, sl, sc, out int sidx)) + return Response.Error($"apply_text_edits: start out of range (line {sl}, col {sc})"); + if (!TryIndexFromLineCol(original, el, ec, out int eidx)) + return Response.Error($"apply_text_edits: end out of range (line {el}, col {ec})"); + if (eidx < sidx) (sidx, eidx) = (eidx, sidx); + + spans.Add((sidx, eidx, newText)); + checked + { + totalBytes += System.Text.Encoding.UTF8.GetByteCount(newText); + } + } + catch (Exception ex) + { + return Response.Error($"Invalid edit payload: {ex.Message}"); + } + } + + // Header guard: refuse edits that touch before the first 'using ' directive (after optional BOM) to prevent file corruption + int headerBoundary = (original.Length > 0 && original[0] == '\uFEFF') ? 1 : 0; // skip BOM once if present + // Find first top-level using (supports alias, static, and dotted namespaces) + var mUsing = System.Text.RegularExpressions.Regex.Match( + original, + @"(?m)^\s*using\s+(?:static\s+)?(?:[A-Za-z_]\w*\s*=\s*)?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*;", + System.Text.RegularExpressions.RegexOptions.CultureInvariant, + TimeSpan.FromSeconds(2) + ); + if (mUsing.Success) + { + headerBoundary = Math.Min(Math.Max(headerBoundary, mUsing.Index), original.Length); + } + foreach (var sp in spans) + { + if (sp.start < headerBoundary) + { + return Response.Error("using_guard", new { status = "using_guard", hint = "Refusing to edit before the first 'using'. Use anchor_insert near a method or a structured edit." }); + } + } + + // Attempt auto-upgrade: if a single edit targets a method header/body, re-route as structured replace_method + if (spans.Count == 1) + { + var sp = spans[0]; + // Heuristic: around the start of the edit, try to match a method header in original + int searchStart = Math.Max(0, sp.start - 200); + int searchEnd = Math.Min(original.Length, sp.start + 200); + string slice = original.Substring(searchStart, searchEnd - searchStart); + var rx = new System.Text.RegularExpressions.Regex(@"(?m)^[\t ]*(?:\[[^\]]+\][\t ]*)*[\t ]*(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial)[\s\S]*?\b([A-Za-z_][A-Za-z0-9_]*)\s*\("); + var mh = rx.Match(slice); + if (mh.Success) + { + string methodName = mh.Groups[1].Value; + // Find class span containing the edit + if (TryComputeClassSpan(original, name, null, out var clsStart, out var clsLen, out _)) + { + if (TryComputeMethodSpan(original, clsStart, clsLen, methodName, null, null, null, out var mStart, out var mLen, out _)) + { + // If the edit overlaps the method span significantly, treat as replace_method + if (sp.start <= mStart + 2 && sp.end >= mStart + 1) + { + var structEdits = new JArray(); + + // Apply the edit to get a candidate string, then recompute method span on the edited text + string candidate = original.Remove(sp.start, sp.end - sp.start).Insert(sp.start, sp.text ?? string.Empty); + string replacementText; + if (TryComputeClassSpan(candidate, name, null, out var cls2Start, out var cls2Len, out _) + && TryComputeMethodSpan(candidate, cls2Start, cls2Len, methodName, null, null, null, out var m2Start, out var m2Len, out _)) + { + replacementText = candidate.Substring(m2Start, m2Len); + } + else + { + // Fallback: adjust method start by the net delta if the edit was before the method + int delta = (sp.text?.Length ?? 0) - (sp.end - sp.start); + int adjustedStart = mStart + (sp.start <= mStart ? delta : 0); + adjustedStart = Math.Max(0, Math.Min(adjustedStart, candidate.Length)); + + // If the edit was within the original method span, adjust the length by the delta within-method + int withinMethodDelta = 0; + if (sp.start >= mStart && sp.start <= mStart + mLen) + { + withinMethodDelta = delta; + } + int adjustedLen = mLen + withinMethodDelta; + adjustedLen = Math.Max(0, Math.Min(candidate.Length - adjustedStart, adjustedLen)); + replacementText = candidate.Substring(adjustedStart, adjustedLen); + } + + var op = new JObject + { + ["mode"] = "replace_method", + ["className"] = name, + ["methodName"] = methodName, + ["replacement"] = replacementText + }; + structEdits.Add(op); + // Reuse structured path + return EditScript(fullPath, relativePath, name, structEdits, new JObject { ["refresh"] = "immediate", ["validate"] = "standard" }); + } + } + } + } + } + + if (totalBytes > MaxEditPayloadBytes) + { + return Response.Error("too_large", new { status = "too_large", limitBytes = MaxEditPayloadBytes, hint = "split into smaller edits" }); + } + + // Ensure non-overlap and apply from back to front + spans = spans.OrderByDescending(t => t.start).ToList(); + for (int i = 1; i < spans.Count; i++) + { + if (spans[i].end > spans[i - 1].start) + { + var conflict = new[] { new { startA = spans[i].start, endA = spans[i].end, startB = spans[i - 1].start, endB = spans[i - 1].end } }; + return Response.Error("overlap", new { status = "overlap", conflicts = conflict, hint = "Sort ranges descending by start and compute from the same snapshot." }); + } + } + + string working = original; + bool relaxed = string.Equals(validateMode, "relaxed", StringComparison.OrdinalIgnoreCase); + bool syntaxOnly = string.Equals(validateMode, "syntax", StringComparison.OrdinalIgnoreCase); + foreach (var sp in spans) + { + string next = working.Remove(sp.start, sp.end - sp.start).Insert(sp.start, sp.text ?? string.Empty); + if (relaxed) + { + // Scoped balance check: validate just around the changed region to avoid false positives + int originalLength = sp.end - sp.start; + int newLength = sp.text?.Length ?? 0; + int endPos = sp.start + newLength; + if (!CheckScopedBalance(next, Math.Max(0, sp.start - 500), Math.Min(next.Length, endPos + 500))) + { + return Response.Error("unbalanced_braces", new { status = "unbalanced_braces", line = 0, expected = "{}()[] (scoped)", hint = "Use standard validation or shrink the edit range." }); + } + } + working = next; + } + + // No-op guard: if resulting text is identical, avoid writes and return explicit no-op + if (string.Equals(working, original, StringComparison.Ordinal)) + { + string noChangeSha = ComputeSha256(original); + return Response.Success( + $"No-op: contents unchanged for '{relativePath}'.", + new + { + uri = $"unity://path/{relativePath}", + path = relativePath, + editsApplied = 0, + no_op = true, + sha256 = noChangeSha, + evidence = new { reason = "identical_content" } + } + ); + } + + // Always check final structural balance regardless of relaxed mode + if (!CheckBalancedDelimiters(working, out int line, out char expected)) + { + int startLine = Math.Max(1, line - 5); + int endLine = line + 5; + string hint = $"unbalanced_braces at line {line}. Call resources/read for lines {startLine}-{endLine} and resend a smaller apply_text_edits that restores balance."; + return Response.Error(hint, new { status = "unbalanced_braces", line, expected = expected.ToString(), evidenceWindow = new { startLine, endLine } }); + } + +#if USE_ROSLYN + if (!syntaxOnly) + { + var tree = CSharpSyntaxTree.ParseText(working); + var diagnostics = tree.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).Take(3) + .Select(d => new { + line = d.Location.GetLineSpan().StartLinePosition.Line + 1, + col = d.Location.GetLineSpan().StartLinePosition.Character + 1, + code = d.Id, + message = d.GetMessage() + }).ToArray(); + if (diagnostics.Length > 0) + { + int firstLine = diagnostics[0].line; + int startLineRos = Math.Max(1, firstLine - 5); + int endLineRos = firstLine + 5; + return Response.Error("syntax_error", new { status = "syntax_error", diagnostics, evidenceWindow = new { startLine = startLineRos, endLine = endLineRos } }); + } + + // Optional formatting + try + { + var root = tree.GetRoot(); + var workspace = new AdhocWorkspace(); + root = Microsoft.CodeAnalysis.Formatting.Formatter.Format(root, workspace); + working = root.ToFullString(); + } + catch { } + } +#endif + + string newSha = ComputeSha256(working); + + // Atomic write and schedule refresh + try + { + var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + var tmp = fullPath + ".tmp"; + File.WriteAllText(tmp, working, enc); + string backup = fullPath + ".bak"; + try + { + File.Replace(tmp, fullPath, backup); + try { if (File.Exists(backup)) File.Delete(backup); } catch { /* ignore */ } + } + catch (PlatformNotSupportedException) + { + File.Copy(tmp, fullPath, true); + try { File.Delete(tmp); } catch { } + try { if (File.Exists(backup)) File.Delete(backup); } catch { } + } + catch (IOException) + { + File.Copy(tmp, fullPath, true); + try { File.Delete(tmp); } catch { } + try { if (File.Exists(backup)) File.Delete(backup); } catch { } + } + + // Respect refresh mode: immediate vs debounced + bool immediate = string.Equals(refreshModeFromCaller, "immediate", StringComparison.OrdinalIgnoreCase) || + string.Equals(refreshModeFromCaller, "sync", StringComparison.OrdinalIgnoreCase); + if (immediate) + { + McpLog.Info($"[ManageScript] ApplyTextEdits: immediate refresh for '{relativePath}'"); + AssetDatabase.ImportAsset( + relativePath, + ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate + ); +#if UNITY_EDITOR + UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); +#endif + } + else + { + McpLog.Info($"[ManageScript] ApplyTextEdits: debounced refresh scheduled for '{relativePath}'"); + ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath); + } + + return Response.Success( + $"Applied {spans.Count} text edit(s) to '{relativePath}'.", + new + { + uri = $"unity://path/{relativePath}", + path = relativePath, + editsApplied = spans.Count, + sha256 = newSha, + scheduledRefresh = !immediate + } + ); + } + catch (Exception ex) + { + return Response.Error($"Failed to write edits: {ex.Message}"); + } + } + + private static bool TryIndexFromLineCol(string text, int line1, int col1, out int index) + { + // 1-based line/col to absolute index (0-based), col positions are counted in code points + int line = 1, col = 1; + for (int i = 0; i <= text.Length; i++) + { + if (line == line1 && col == col1) + { + index = i; + return true; + } + if (i == text.Length) break; + char c = text[i]; + if (c == '\r') + { + // Treat CRLF as a single newline; skip the LF if present + if (i + 1 < text.Length && text[i + 1] == '\n') + i++; + line++; + col = 1; + } + else if (c == '\n') + { + line++; + col = 1; + } + else + { + col++; + } + } + index = -1; + return false; + } + + private static string ComputeSha256(string contents) + { + using (var sha = SHA256.Create()) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(contents); + var hash = sha.ComputeHash(bytes); + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + } + } + + private static bool CheckBalancedDelimiters(string text, out int line, out char expected) + { + var braceStack = new Stack(); + var parenStack = new Stack(); + var bracketStack = new Stack(); + bool inString = false, inChar = false, inSingle = false, inMulti = false, escape = false; + line = 1; expected = '\0'; + + for (int i = 0; i < text.Length; i++) + { + char c = text[i]; + char next = i + 1 < text.Length ? text[i + 1] : '\0'; + + if (c == '\n') { line++; if (inSingle) inSingle = false; } + + if (escape) { escape = false; continue; } + + if (inString) + { + if (c == '\\') { escape = true; } + else if (c == '"') inString = false; + continue; + } + if (inChar) + { + if (c == '\\') { escape = true; } + else if (c == '\'') inChar = false; + continue; + } + if (inSingle) continue; + if (inMulti) + { + if (c == '*' && next == '/') { inMulti = false; i++; } + continue; + } + + if (c == '"') { inString = true; continue; } + if (c == '\'') { inChar = true; continue; } + if (c == '/' && next == '/') { inSingle = true; i++; continue; } + if (c == '/' && next == '*') { inMulti = true; i++; continue; } + + switch (c) + { + case '{': braceStack.Push(line); break; + case '}': + if (braceStack.Count == 0) { expected = '{'; return false; } + braceStack.Pop(); + break; + case '(': parenStack.Push(line); break; + case ')': + if (parenStack.Count == 0) { expected = '('; return false; } + parenStack.Pop(); + break; + case '[': bracketStack.Push(line); break; + case ']': + if (bracketStack.Count == 0) { expected = '['; return false; } + bracketStack.Pop(); + break; + } + } + + if (braceStack.Count > 0) { line = braceStack.Peek(); expected = '}'; return false; } + if (parenStack.Count > 0) { line = parenStack.Peek(); expected = ')'; return false; } + if (bracketStack.Count > 0) { line = bracketStack.Peek(); expected = ']'; return false; } + + return true; + } + + // Lightweight scoped balance: checks delimiters within a substring, ignoring outer context + private static bool CheckScopedBalance(string text, int start, int end) + { + start = Math.Max(0, Math.Min(text.Length, start)); + end = Math.Max(start, Math.Min(text.Length, end)); + int brace = 0, paren = 0, bracket = 0; + bool inStr = false, inChr = false, esc = false; + for (int i = start; i < end; i++) + { + char c = text[i]; + char n = (i + 1 < end) ? text[i + 1] : '\0'; + if (inStr) + { + if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; + } + if (inChr) + { + if (!esc && c == '\'') inChr = false; esc = (!esc && c == '\\'); continue; + } + if (c == '"') { inStr = true; esc = false; continue; } + if (c == '\'') { inChr = true; esc = false; continue; } + if (c == '/' && n == '/') { while (i < end && text[i] != '\n') i++; continue; } + if (c == '/' && n == '*') { i += 2; while (i + 1 < end && !(text[i] == '*' && text[i + 1] == '/')) i++; i++; continue; } + if (c == '{') brace++; + else if (c == '}') brace--; + else if (c == '(') paren++; + else if (c == ')') paren--; + else if (c == '[') bracket++; else if (c == ']') bracket--; + // Allow temporary negative balance - will check tolerance at end + } + return brace >= -3 && paren >= -3 && bracket >= -3; // tolerate more context from outside region + } + + private static object DeleteScript(string fullPath, string relativePath) + { + if (!File.Exists(fullPath)) + { + return Response.Error($"Script not found at '{relativePath}'. Cannot delete."); + } + + try + { + // Use AssetDatabase.MoveAssetToTrash for safer deletion (allows undo) + bool deleted = AssetDatabase.MoveAssetToTrash(relativePath); + if (deleted) + { + AssetDatabase.Refresh(); + return Response.Success( + $"Script '{Path.GetFileName(relativePath)}' moved to trash successfully.", + new { deleted = true } + ); + } + else + { + // Fallback or error if MoveAssetToTrash fails + return Response.Error( + $"Failed to move script '{relativePath}' to trash. It might be locked or in use." + ); + } + } + catch (Exception e) + { + return Response.Error($"Error deleting script '{relativePath}': {e.Message}"); + } + } + + /// + /// Structured edits (AST-backed where available) on existing scripts. + /// Supports class-level replace/delete with Roslyn span computation if USE_ROSLYN is defined, + /// otherwise falls back to a conservative balanced-brace scan. + /// + private static object EditScript( + string fullPath, + string relativePath, + string name, + JArray edits, + JObject options) + { + if (!File.Exists(fullPath)) + return Response.Error($"Script not found at '{relativePath}'."); + // Refuse edits if the target is a symlink + try + { + var attrs = File.GetAttributes(fullPath); + if ((attrs & FileAttributes.ReparsePoint) != 0) + return Response.Error("Refusing to edit a symlinked script path."); + } + catch + { + // ignore failures checking attributes and proceed + } + if (edits == null || edits.Count == 0) + return Response.Error("No edits provided."); + + string original; + try { original = File.ReadAllText(fullPath); } + catch (Exception ex) { return Response.Error($"Failed to read script: {ex.Message}"); } + + string working = original; + + try + { + var replacements = new List<(int start, int length, string text)>(); + int appliedCount = 0; + + // Apply mode: atomic (default) computes all spans against original and applies together. + // Sequential applies each edit immediately to the current working text (useful for dependent edits). + string applyMode = options?["applyMode"]?.ToString()?.ToLowerInvariant(); + bool applySequentially = applyMode == "sequential"; + + foreach (var e in edits) + { + var op = (JObject)e; + var mode = (op.Value("mode") ?? op.Value("op") ?? string.Empty).ToLowerInvariant(); + + switch (mode) + { + case "replace_class": + { + string className = op.Value("className"); + string ns = op.Value("namespace"); + string replacement = ExtractReplacement(op); + + if (string.IsNullOrWhiteSpace(className)) + return Response.Error("replace_class requires 'className'."); + if (replacement == null) + return Response.Error("replace_class requires 'replacement' (inline or base64)."); + + if (!TryComputeClassSpan(working, className, ns, out var spanStart, out var spanLength, out var why)) + return Response.Error($"replace_class failed: {why}"); + + if (!ValidateClassSnippet(replacement, className, out var vErr)) + return Response.Error($"Replacement snippet invalid: {vErr}"); + + if (applySequentially) + { + working = working.Remove(spanStart, spanLength).Insert(spanStart, NormalizeNewlines(replacement)); + appliedCount++; + } + else + { + replacements.Add((spanStart, spanLength, NormalizeNewlines(replacement))); + } + break; + } + + case "delete_class": + { + string className = op.Value("className"); + string ns = op.Value("namespace"); + if (string.IsNullOrWhiteSpace(className)) + return Response.Error("delete_class requires 'className'."); + + if (!TryComputeClassSpan(working, className, ns, out var s, out var l, out var why)) + return Response.Error($"delete_class failed: {why}"); + + if (applySequentially) + { + working = working.Remove(s, l); + appliedCount++; + } + else + { + replacements.Add((s, l, string.Empty)); + } + break; + } + + case "replace_method": + { + string className = op.Value("className"); + string ns = op.Value("namespace"); + string methodName = op.Value("methodName"); + string replacement = ExtractReplacement(op); + string returnType = op.Value("returnType"); + string parametersSignature = op.Value("parametersSignature"); + string attributesContains = op.Value("attributesContains"); + + if (string.IsNullOrWhiteSpace(className)) return Response.Error("replace_method requires 'className'."); + if (string.IsNullOrWhiteSpace(methodName)) return Response.Error("replace_method requires 'methodName'."); + if (replacement == null) return Response.Error("replace_method requires 'replacement' (inline or base64)."); + + if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass)) + return Response.Error($"replace_method failed to locate class: {whyClass}"); + + if (!TryComputeMethodSpan(working, clsStart, clsLen, methodName, returnType, parametersSignature, attributesContains, out var mStart, out var mLen, out var whyMethod)) + { + bool hasDependentInsert = edits.Any(j => j is JObject jo && + string.Equals(jo.Value("className"), className, StringComparison.Ordinal) && + string.Equals(jo.Value("methodName"), methodName, StringComparison.Ordinal) && + ((jo.Value("mode") ?? jo.Value("op") ?? string.Empty).ToLowerInvariant() == "insert_method")); + string hint = hasDependentInsert && !applySequentially ? " Hint: This batch inserts this method. Use options.applyMode='sequential' or split into separate calls." : string.Empty; + return Response.Error($"replace_method failed: {whyMethod}.{hint}"); + } + + if (applySequentially) + { + working = working.Remove(mStart, mLen).Insert(mStart, NormalizeNewlines(replacement)); + appliedCount++; + } + else + { + replacements.Add((mStart, mLen, NormalizeNewlines(replacement))); + } + break; + } + + case "delete_method": + { + string className = op.Value("className"); + string ns = op.Value("namespace"); + string methodName = op.Value("methodName"); + string returnType = op.Value("returnType"); + string parametersSignature = op.Value("parametersSignature"); + string attributesContains = op.Value("attributesContains"); + + if (string.IsNullOrWhiteSpace(className)) return Response.Error("delete_method requires 'className'."); + if (string.IsNullOrWhiteSpace(methodName)) return Response.Error("delete_method requires 'methodName'."); + + if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass)) + return Response.Error($"delete_method failed to locate class: {whyClass}"); + + if (!TryComputeMethodSpan(working, clsStart, clsLen, methodName, returnType, parametersSignature, attributesContains, out var mStart, out var mLen, out var whyMethod)) + { + bool hasDependentInsert = edits.Any(j => j is JObject jo && + string.Equals(jo.Value("className"), className, StringComparison.Ordinal) && + string.Equals(jo.Value("methodName"), methodName, StringComparison.Ordinal) && + ((jo.Value("mode") ?? jo.Value("op") ?? string.Empty).ToLowerInvariant() == "insert_method")); + string hint = hasDependentInsert && !applySequentially ? " Hint: This batch inserts this method. Use options.applyMode='sequential' or split into separate calls." : string.Empty; + return Response.Error($"delete_method failed: {whyMethod}.{hint}"); + } + + if (applySequentially) + { + working = working.Remove(mStart, mLen); + appliedCount++; + } + else + { + replacements.Add((mStart, mLen, string.Empty)); + } + break; + } + + case "insert_method": + { + string className = op.Value("className"); + string ns = op.Value("namespace"); + string position = (op.Value("position") ?? "end").ToLowerInvariant(); + string afterMethodName = op.Value("afterMethodName"); + string afterReturnType = op.Value("afterReturnType"); + string afterParameters = op.Value("afterParametersSignature"); + string afterAttributesContains = op.Value("afterAttributesContains"); + string snippet = ExtractReplacement(op); + // Harden: refuse empty replacement for inserts + if (snippet == null || snippet.Trim().Length == 0) + return Response.Error("insert_method requires a non-empty 'replacement' text."); + + if (string.IsNullOrWhiteSpace(className)) return Response.Error("insert_method requires 'className'."); + if (snippet == null) return Response.Error("insert_method requires 'replacement' (inline or base64) containing a full method declaration."); + + if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass)) + return Response.Error($"insert_method failed to locate class: {whyClass}"); + + if (position == "after") + { + if (string.IsNullOrEmpty(afterMethodName)) return Response.Error("insert_method with position='after' requires 'afterMethodName'."); + if (!TryComputeMethodSpan(working, clsStart, clsLen, afterMethodName, afterReturnType, afterParameters, afterAttributesContains, out var aStart, out var aLen, out var whyAfter)) + return Response.Error($"insert_method(after) failed to locate anchor method: {whyAfter}"); + int insAt = aStart + aLen; + string text = NormalizeNewlines("\n\n" + snippet.TrimEnd() + "\n"); + if (applySequentially) + { + working = working.Insert(insAt, text); + appliedCount++; + } + else + { + replacements.Add((insAt, 0, text)); + } + } + else if (!TryFindClassInsertionPoint(working, clsStart, clsLen, position, out var insAt, out var whyIns)) + return Response.Error($"insert_method failed: {whyIns}"); + else + { + string text = NormalizeNewlines("\n\n" + snippet.TrimEnd() + "\n"); + if (applySequentially) + { + working = working.Insert(insAt, text); + appliedCount++; + } + else + { + replacements.Add((insAt, 0, text)); + } + } + break; + } + + case "anchor_insert": + { + string anchor = op.Value("anchor"); + string position = (op.Value("position") ?? "before").ToLowerInvariant(); + string text = op.Value("text") ?? ExtractReplacement(op); + if (string.IsNullOrWhiteSpace(anchor)) return Response.Error("anchor_insert requires 'anchor' (regex)."); + if (string.IsNullOrEmpty(text)) return Response.Error("anchor_insert requires non-empty 'text'."); + + try + { + var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2)); + var m = rx.Match(working); + if (!m.Success) return Response.Error($"anchor_insert: anchor not found: {anchor}"); + int insAt = position == "after" ? m.Index + m.Length : m.Index; + string norm = NormalizeNewlines(text); + if (!norm.EndsWith("\n")) + { + norm += "\n"; + } + + // Duplicate guard: if identical snippet already exists within this class, skip insert + if (TryComputeClassSpan(working, name, null, out var clsStartDG, out var clsLenDG, out _)) + { + string classSlice = working.Substring(clsStartDG, Math.Min(clsLenDG, working.Length - clsStartDG)); + if (classSlice.IndexOf(norm, StringComparison.Ordinal) >= 0) + { + // Do not insert duplicate; treat as no-op + break; + } + } + if (applySequentially) + { + working = working.Insert(insAt, norm); + appliedCount++; + } + else + { + replacements.Add((insAt, 0, norm)); + } + } + catch (Exception ex) + { + return Response.Error($"anchor_insert failed: {ex.Message}"); + } + break; + } + + case "anchor_delete": + { + string anchor = op.Value("anchor"); + if (string.IsNullOrWhiteSpace(anchor)) return Response.Error("anchor_delete requires 'anchor' (regex)."); + try + { + var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2)); + var m = rx.Match(working); + if (!m.Success) return Response.Error($"anchor_delete: anchor not found: {anchor}"); + int delAt = m.Index; + int delLen = m.Length; + if (applySequentially) + { + working = working.Remove(delAt, delLen); + appliedCount++; + } + else + { + replacements.Add((delAt, delLen, string.Empty)); + } + } + catch (Exception ex) + { + return Response.Error($"anchor_delete failed: {ex.Message}"); + } + break; + } + + case "anchor_replace": + { + string anchor = op.Value("anchor"); + string replacement = op.Value("text") ?? op.Value("replacement") ?? ExtractReplacement(op) ?? string.Empty; + if (string.IsNullOrWhiteSpace(anchor)) return Response.Error("anchor_replace requires 'anchor' (regex)."); + try + { + var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2)); + var m = rx.Match(working); + if (!m.Success) return Response.Error($"anchor_replace: anchor not found: {anchor}"); + int at = m.Index; + int len = m.Length; + string norm = NormalizeNewlines(replacement); + if (applySequentially) + { + working = working.Remove(at, len).Insert(at, norm); + appliedCount++; + } + else + { + replacements.Add((at, len, norm)); + } + } + catch (Exception ex) + { + return Response.Error($"anchor_replace failed: {ex.Message}"); + } + break; + } + + default: + return Response.Error($"Unknown edit mode: '{mode}'. Allowed: replace_class, delete_class, replace_method, delete_method, insert_method, anchor_insert, anchor_delete, anchor_replace."); + } + } + + if (!applySequentially) + { + if (HasOverlaps(replacements)) + { + var ordered = replacements.OrderByDescending(r => r.start).ToList(); + for (int i = 1; i < ordered.Count; i++) + { + if (ordered[i].start + ordered[i].length > ordered[i - 1].start) + { + var conflict = new[] { new { startA = ordered[i].start, endA = ordered[i].start + ordered[i].length, startB = ordered[i - 1].start, endB = ordered[i - 1].start + ordered[i - 1].length } }; + return Response.Error("overlap", new { status = "overlap", conflicts = conflict, hint = "Sort ranges descending by start and compute from the same snapshot." }); + } + } + return Response.Error("overlap", new { status = "overlap" }); + } + + foreach (var r in replacements.OrderByDescending(r => r.start)) + working = working.Remove(r.start, r.length).Insert(r.start, r.text); + appliedCount = replacements.Count; + } + + // Guard against structural imbalance before validation + if (!CheckBalancedDelimiters(working, out int lineBal, out char expectedBal)) + return Response.Error("unbalanced_braces", new { status = "unbalanced_braces", line = lineBal, expected = expectedBal.ToString() }); + + // No-op guard for structured edits: if text unchanged, return explicit no-op + if (string.Equals(working, original, StringComparison.Ordinal)) + { + var sameSha = ComputeSha256(original); + return Response.Success( + $"No-op: contents unchanged for '{relativePath}'.", + new + { + path = relativePath, + uri = $"unity://path/{relativePath}", + editsApplied = 0, + no_op = true, + sha256 = sameSha, + evidence = new { reason = "identical_content" } + } + ); + } + + // Validate result using override from options if provided; otherwise GUI strictness + var level = GetValidationLevelFromGUI(); + try + { + var validateOpt = options?["validate"]?.ToString()?.ToLowerInvariant(); + if (!string.IsNullOrEmpty(validateOpt)) + { + level = validateOpt switch + { + "basic" => ValidationLevel.Basic, + "standard" => ValidationLevel.Standard, + "comprehensive" => ValidationLevel.Comprehensive, + "strict" => ValidationLevel.Strict, + _ => level + }; + } + } + catch { /* ignore option parsing issues */ } + if (!ValidateScriptSyntax(working, level, out var errors)) + return Response.Error("validation_failed", new { status = "validation_failed", diagnostics = errors ?? Array.Empty() }); + else if (errors != null && errors.Length > 0) + Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", errors)); + + // Atomic write with backup; schedule refresh + // Decide refresh behavior + string refreshMode = options?["refresh"]?.ToString()?.ToLowerInvariant(); + bool immediate = refreshMode == "immediate" || refreshMode == "sync"; + + // Persist changes atomically (no BOM), then compute/return new file SHA + var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + var tmp = fullPath + ".tmp"; + File.WriteAllText(tmp, working, enc); + var backup = fullPath + ".bak"; + try + { + File.Replace(tmp, fullPath, backup); + try { if (File.Exists(backup)) File.Delete(backup); } catch { } + } + catch (PlatformNotSupportedException) + { + File.Copy(tmp, fullPath, true); + try { File.Delete(tmp); } catch { } + try { if (File.Exists(backup)) File.Delete(backup); } catch { } + } + catch (IOException) + { + File.Copy(tmp, fullPath, true); + try { File.Delete(tmp); } catch { } + try { if (File.Exists(backup)) File.Delete(backup); } catch { } + } + + var newSha = ComputeSha256(working); + var ok = Response.Success( + $"Applied {appliedCount} structured edit(s) to '{relativePath}'.", + new + { + path = relativePath, + uri = $"unity://path/{relativePath}", + editsApplied = appliedCount, + scheduledRefresh = !immediate, + sha256 = newSha + } + ); + + if (immediate) + { + McpLog.Info($"[ManageScript] EditScript: immediate refresh for '{relativePath}'", always: false); + ManageScriptRefreshHelpers.ImportAndRequestCompile(relativePath); + } + else + { + ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath); + } + return ok; + } + catch (Exception ex) + { + return Response.Error($"Edit failed: {ex.Message}"); + } + } + + private static bool HasOverlaps(IEnumerable<(int start, int length, string text)> list) + { + var arr = list.OrderBy(x => x.start).ToArray(); + for (int i = 1; i < arr.Length; i++) + { + if (arr[i - 1].start + arr[i - 1].length > arr[i].start) + return true; + } + return false; + } + + private static string ExtractReplacement(JObject op) + { + var inline = op.Value("replacement"); + if (!string.IsNullOrEmpty(inline)) return inline; + + var b64 = op.Value("replacementBase64"); + if (!string.IsNullOrEmpty(b64)) + { + try { return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(b64)); } + catch { return null; } + } + return null; + } + + private static string NormalizeNewlines(string t) + { + if (string.IsNullOrEmpty(t)) return t; + return t.Replace("\r\n", "\n").Replace("\r", "\n"); + } + + private static bool ValidateClassSnippet(string snippet, string expectedName, out string err) + { +#if USE_ROSLYN + try + { + var tree = CSharpSyntaxTree.ParseText(snippet); + var root = tree.GetRoot(); + var classes = root.DescendantNodes().OfType().ToList(); + if (classes.Count != 1) { err = "snippet must contain exactly one class declaration"; return false; } + // Optional: enforce expected name + // if (classes[0].Identifier.ValueText != expectedName) { err = $"snippet declares '{classes[0].Identifier.ValueText}', expected '{expectedName}'"; return false; } + err = null; return true; + } + catch (Exception ex) { err = ex.Message; return false; } +#else + if (string.IsNullOrWhiteSpace(snippet) || !snippet.Contains("class ")) { err = "no 'class' keyword found in snippet"; return false; } + err = null; return true; +#endif + } + + private static bool TryComputeClassSpan(string source, string className, string ns, out int start, out int length, out string why) + { +#if USE_ROSLYN + try + { + var tree = CSharpSyntaxTree.ParseText(source); + var root = tree.GetRoot(); + var classes = root.DescendantNodes() + .OfType() + .Where(c => c.Identifier.ValueText == className); + + if (!string.IsNullOrEmpty(ns)) + { + classes = classes.Where(c => + (c.FirstAncestorOrSelf()?.Name?.ToString() ?? "") == ns + || (c.FirstAncestorOrSelf()?.Name?.ToString() ?? "") == ns); + } + + var list = classes.ToList(); + if (list.Count == 0) { start = length = 0; why = $"class '{className}' not found" + (ns != null ? $" in namespace '{ns}'" : ""); return false; } + if (list.Count > 1) { start = length = 0; why = $"class '{className}' matched {list.Count} declarations (partial/nested?). Disambiguate."; return false; } + + var cls = list[0]; + var span = cls.FullSpan; // includes attributes & leading trivia + start = span.Start; length = span.Length; why = null; return true; + } + catch + { + // fall back below + } +#endif + return TryComputeClassSpanBalanced(source, className, ns, out start, out length, out why); + } + + private static bool TryComputeClassSpanBalanced(string source, string className, string ns, out int start, out int length, out string why) + { + start = length = 0; why = null; + var idx = IndexOfClassToken(source, className); + if (idx < 0) { why = $"class '{className}' not found (balanced scan)"; return false; } + + if (!string.IsNullOrEmpty(ns) && !AppearsWithinNamespaceHeader(source, idx, ns)) + { why = $"class '{className}' not under namespace '{ns}' (balanced scan)"; return false; } + + // Include modifiers/attributes on the same line: back up to the start of line + int lineStart = idx; + while (lineStart > 0 && source[lineStart - 1] != '\n' && source[lineStart - 1] != '\r') lineStart--; + + int i = idx; + while (i < source.Length && source[i] != '{') i++; + if (i >= source.Length) { why = "no opening brace after class header"; return false; } + + int depth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false; + int startSpan = lineStart; + for (; i < source.Length; i++) + { + char c = source[i]; + char n = i + 1 < source.Length ? source[i + 1] : '\0'; + + if (inSL) { if (c == '\n') inSL = false; continue; } + if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; } + if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; } + if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; } + + if (c == '/' && n == '/') { inSL = true; i++; continue; } + if (c == '/' && n == '*') { inML = true; i++; continue; } + if (c == '"') { inStr = true; continue; } + if (c == '\'') { inChar = true; continue; } + + if (c == '{') { depth++; } + else if (c == '}') + { + depth--; + if (depth == 0) { start = startSpan; length = (i - startSpan) + 1; return true; } + if (depth < 0) { why = "brace underflow"; return false; } + } + } + why = "unterminated class block"; return false; + } + + private static bool TryComputeMethodSpan( + string source, + int classStart, + int classLength, + string methodName, + string returnType, + string parametersSignature, + string attributesContains, + out int start, + out int length, + out string why) + { + start = length = 0; why = null; + int searchStart = classStart; + int searchEnd = Math.Min(source.Length, classStart + classLength); + + // 1) Find the method header using a stricter regex (allows optional attributes above) + string rtPattern = string.IsNullOrEmpty(returnType) ? @"[^\s]+" : Regex.Escape(returnType).Replace("\\ ", "\\s+"); + string namePattern = Regex.Escape(methodName); + // If a parametersSignature is provided, it may include surrounding parentheses. Strip them so + // we can safely embed the signature inside our own parenthesis group without duplicating. + string paramsPattern; + if (string.IsNullOrEmpty(parametersSignature)) + { + paramsPattern = @"[\s\S]*?"; // permissive when not specified + } + else + { + string ps = parametersSignature.Trim(); + if (ps.StartsWith("(") && ps.EndsWith(")") && ps.Length >= 2) + { + ps = ps.Substring(1, ps.Length - 2); + } + // Escape literal text of the signature + paramsPattern = Regex.Escape(ps); + } + string pattern = + @"(?m)^[\t ]*(?:\[[^\]]+\][\t ]*)*[\t ]*" + + @"(?:(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial|readonly|volatile|event|abstract|ref|in|out)\s+)*" + + rtPattern + @"[\t ]+" + namePattern + @"\s*(?:<[^>]+>)?\s*\(" + paramsPattern + @"\)"; + + string slice = source.Substring(searchStart, searchEnd - searchStart); + var headerMatch = Regex.Match(slice, pattern, RegexOptions.Multiline, TimeSpan.FromSeconds(2)); + if (!headerMatch.Success) + { + why = $"method '{methodName}' header not found in class"; return false; + } + int headerIndex = searchStart + headerMatch.Index; + + // Optional attributes filter: look upward from headerIndex for contiguous attribute lines + if (!string.IsNullOrEmpty(attributesContains)) + { + int attrScanStart = headerIndex; + while (attrScanStart > searchStart) + { + int prevNl = source.LastIndexOf('\n', attrScanStart - 1); + if (prevNl < 0 || prevNl < searchStart) break; + string prevLine = source.Substring(prevNl + 1, attrScanStart - (prevNl + 1)); + if (prevLine.TrimStart().StartsWith("[")) { attrScanStart = prevNl; continue; } + break; + } + string attrBlock = source.Substring(attrScanStart, headerIndex - attrScanStart); + if (attrBlock.IndexOf(attributesContains, StringComparison.Ordinal) < 0) + { + why = $"method '{methodName}' found but attributes filter did not match"; return false; + } + } + + // backtrack to the very start of header/attributes to include in span + int lineStart = headerIndex; + while (lineStart > searchStart && source[lineStart - 1] != '\n' && source[lineStart - 1] != '\r') lineStart--; + // If previous lines are attributes, include them + int attrStart = lineStart; + int probe = lineStart - 1; + while (probe > searchStart) + { + int prevNl = source.LastIndexOf('\n', probe); + if (prevNl < 0 || prevNl < searchStart) break; + string prev = source.Substring(prevNl + 1, attrStart - (prevNl + 1)); + if (prev.TrimStart().StartsWith("[")) { attrStart = prevNl + 1; probe = prevNl - 1; } + else break; + } + + // 2) Walk from the end of signature to detect body style ('{' or '=> ...;') and compute end + // Find the '(' that belongs to the method signature, not attributes + int nameTokenIdx = IndexOfTokenWithin(source, methodName, headerIndex, searchEnd); + if (nameTokenIdx < 0) { why = $"method '{methodName}' token not found after header"; return false; } + int sigOpenParen = IndexOfTokenWithin(source, "(", nameTokenIdx, searchEnd); + if (sigOpenParen < 0) { why = "method parameter list '(' not found"; return false; } + + int i = sigOpenParen; + int parenDepth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false; + for (; i < searchEnd; i++) + { + char c = source[i]; + char n = i + 1 < searchEnd ? source[i + 1] : '\0'; + if (inSL) { if (c == '\n') inSL = false; continue; } + if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; } + if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; } + if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; } + + if (c == '/' && n == '/') { inSL = true; i++; continue; } + if (c == '/' && n == '*') { inML = true; i++; continue; } + if (c == '"') { inStr = true; continue; } + if (c == '\'') { inChar = true; continue; } + + if (c == '(') parenDepth++; + if (c == ')') { parenDepth--; if (parenDepth == 0) { i++; break; } } + } + + // After params: detect expression-bodied or block-bodied + // Skip whitespace/comments + for (; i < searchEnd; i++) + { + char c = source[i]; + char n = i + 1 < searchEnd ? source[i + 1] : '\0'; + if (char.IsWhiteSpace(c)) continue; + if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; } + if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; } + break; + } + + // Tolerate generic constraints between params and body: multiple 'where T : ...' + for (; ; ) + { + // Skip whitespace/comments before checking for 'where' + for (; i < searchEnd; i++) + { + char c = source[i]; + char n = i + 1 < searchEnd ? source[i + 1] : '\0'; + if (char.IsWhiteSpace(c)) continue; + if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; } + if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; } + break; + } + + // Check word-boundary 'where' + bool hasWhere = false; + if (i + 5 <= searchEnd) + { + hasWhere = source[i] == 'w' && source[i + 1] == 'h' && source[i + 2] == 'e' && source[i + 3] == 'r' && source[i + 4] == 'e'; + if (hasWhere) + { + // Left boundary + if (i - 1 >= 0) + { + char lb = source[i - 1]; + if (char.IsLetterOrDigit(lb) || lb == '_') hasWhere = false; + } + // Right boundary + if (hasWhere && i + 5 < searchEnd) + { + char rb = source[i + 5]; + if (char.IsLetterOrDigit(rb) || rb == '_') hasWhere = false; + } + } + } + if (!hasWhere) break; + + // Advance past the entire where-constraint clause until we hit '{' or '=>' or ';' + i += 5; // past 'where' + while (i < searchEnd) + { + char c = source[i]; + char n = i + 1 < searchEnd ? source[i + 1] : '\0'; + if (c == '{' || c == ';' || (c == '=' && n == '>')) break; + // Skip comments inline + if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; } + if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; } + i++; + } + } + + // Re-check for expression-bodied after constraints + if (i < searchEnd - 1 && source[i] == '=' && source[i + 1] == '>') + { + // expression-bodied method: seek to terminating semicolon + int j = i; + bool done = false; + while (j < searchEnd) + { + char c = source[j]; + if (c == ';') { done = true; break; } + j++; + } + if (!done) { why = "unterminated expression-bodied method"; return false; } + start = attrStart; length = (j - attrStart) + 1; return true; + } + + if (i >= searchEnd || source[i] != '{') { why = "no opening brace after method signature"; return false; } + + int depth = 0; inStr = false; inChar = false; inSL = false; inML = false; esc = false; + int startSpan = attrStart; + for (; i < searchEnd; i++) + { + char c = source[i]; + char n = i + 1 < searchEnd ? source[i + 1] : '\0'; + if (inSL) { if (c == '\n') inSL = false; continue; } + if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; } + if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; } + if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; } + + if (c == '/' && n == '/') { inSL = true; i++; continue; } + if (c == '/' && n == '*') { inML = true; i++; continue; } + if (c == '"') { inStr = true; continue; } + if (c == '\'') { inChar = true; continue; } + + if (c == '{') depth++; + else if (c == '}') + { + depth--; + if (depth == 0) { start = startSpan; length = (i - startSpan) + 1; return true; } + if (depth < 0) { why = "brace underflow in method"; return false; } + } + } + why = "unterminated method block"; return false; + } + + private static int IndexOfTokenWithin(string s, string token, int start, int end) + { + int idx = s.IndexOf(token, start, StringComparison.Ordinal); + return (idx >= 0 && idx < end) ? idx : -1; + } + + private static bool TryFindClassInsertionPoint(string source, int classStart, int classLength, string position, out int insertAt, out string why) + { + insertAt = 0; why = null; + int searchStart = classStart; + int searchEnd = Math.Min(source.Length, classStart + classLength); + + if (position == "start") + { + // find first '{' after class header, insert just after with a newline + int i = IndexOfTokenWithin(source, "{", searchStart, searchEnd); + if (i < 0) { why = "could not find class opening brace"; return false; } + insertAt = i + 1; return true; + } + else // end + { + // walk to matching closing brace of class and insert just before it + int i = IndexOfTokenWithin(source, "{", searchStart, searchEnd); + if (i < 0) { why = "could not find class opening brace"; return false; } + int depth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false; + for (; i < searchEnd; i++) + { + char c = source[i]; + char n = i + 1 < searchEnd ? source[i + 1] : '\0'; + if (inSL) { if (c == '\n') inSL = false; continue; } + if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; } + if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; } + if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; } + + if (c == '/' && n == '/') { inSL = true; i++; continue; } + if (c == '/' && n == '*') { inML = true; i++; continue; } + if (c == '"') { inStr = true; continue; } + if (c == '\'') { inChar = true; continue; } + + if (c == '{') depth++; + else if (c == '}') + { + depth--; + if (depth == 0) { insertAt = i; return true; } + if (depth < 0) { why = "brace underflow while scanning class"; return false; } + } + } + why = "could not find class closing brace"; return false; + } + } + + private static int IndexOfClassToken(string s, string className) + { + // simple token search; could be tightened with Regex for word boundaries + var pattern = "class " + className; + return s.IndexOf(pattern, StringComparison.Ordinal); + } + + private static bool AppearsWithinNamespaceHeader(string s, int pos, string ns) + { + int from = Math.Max(0, pos - 2000); + var slice = s.Substring(from, pos - from); + return slice.Contains("namespace " + ns); + } + + /// + /// Generates basic C# script content based on name and type. + /// + private static string GenerateDefaultScriptContent( + string name, + string scriptType, + string namespaceName + ) + { + string usingStatements = "using UnityEngine;\nusing System.Collections;\n"; + string classDeclaration; + string body = + "\n // Use this for initialization\n void Start() {\n\n }\n\n // Update is called once per frame\n void Update() {\n\n }\n"; + + string baseClass = ""; + if (!string.IsNullOrEmpty(scriptType)) + { + if (scriptType.Equals("MonoBehaviour", StringComparison.OrdinalIgnoreCase)) + baseClass = " : MonoBehaviour"; + else if (scriptType.Equals("ScriptableObject", StringComparison.OrdinalIgnoreCase)) + { + baseClass = " : ScriptableObject"; + body = ""; // ScriptableObjects don't usually need Start/Update + } + else if ( + scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase) + || scriptType.Equals("EditorWindow", StringComparison.OrdinalIgnoreCase) + ) + { + usingStatements += "using UnityEditor;\n"; + if (scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase)) + baseClass = " : Editor"; + else + baseClass = " : EditorWindow"; + body = ""; // Editor scripts have different structures + } + // Add more types as needed + } + + classDeclaration = $"public class {name}{baseClass}"; + + string fullContent = $"{usingStatements}\n"; + bool useNamespace = !string.IsNullOrEmpty(namespaceName); + + if (useNamespace) + { + fullContent += $"namespace {namespaceName}\n{{\n"; + // Indent class and body if using namespace + classDeclaration = " " + classDeclaration; + body = string.Join("\n", body.Split('\n').Select(line => " " + line)); + } + + fullContent += $"{classDeclaration}\n{{\n{body}\n}}"; + + if (useNamespace) + { + fullContent += "\n}"; // Close namespace + } + + return fullContent.Trim() + "\n"; // Ensure a trailing newline + } + + /// + /// Gets the validation level from the GUI settings + /// + private static ValidationLevel GetValidationLevelFromGUI() + { + string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); + return savedLevel.ToLower() switch + { + "basic" => ValidationLevel.Basic, + "standard" => ValidationLevel.Standard, + "comprehensive" => ValidationLevel.Comprehensive, + "strict" => ValidationLevel.Strict, + _ => ValidationLevel.Standard // Default fallback + }; + } + + /// + /// Validates C# script syntax using multiple validation layers. + /// + private static bool ValidateScriptSyntax(string contents) + { + return ValidateScriptSyntax(contents, ValidationLevel.Standard, out _); + } + + /// + /// Advanced syntax validation with detailed diagnostics and configurable strictness. + /// + private static bool ValidateScriptSyntax(string contents, ValidationLevel level, out string[] errors) + { + var errorList = new System.Collections.Generic.List(); + errors = null; + + if (string.IsNullOrEmpty(contents)) + { + return true; // Empty content is valid + } + + // Basic structural validation + if (!ValidateBasicStructure(contents, errorList)) + { + errors = errorList.ToArray(); + return false; + } + +#if USE_ROSLYN + // Advanced Roslyn-based validation: only run for Standard+; fail on Roslyn errors + if (level >= ValidationLevel.Standard) + { + if (!ValidateScriptSyntaxRoslyn(contents, level, errorList)) + { + errors = errorList.ToArray(); + return false; + } + } +#endif + + // Unity-specific validation + if (level >= ValidationLevel.Standard) + { + ValidateScriptSyntaxUnity(contents, errorList); + } + + // Semantic analysis for common issues + if (level >= ValidationLevel.Comprehensive) + { + ValidateSemanticRules(contents, errorList); + } + +#if USE_ROSLYN + // Full semantic compilation validation for Strict level + if (level == ValidationLevel.Strict) + { + if (!ValidateScriptSemantics(contents, errorList)) + { + errors = errorList.ToArray(); + return false; // Strict level fails on any semantic errors + } + } +#endif + + errors = errorList.ToArray(); + return errorList.Count == 0 || (level != ValidationLevel.Strict && !errorList.Any(e => e.StartsWith("ERROR:"))); + } + + /// + /// Validation strictness levels + /// + private enum ValidationLevel + { + Basic, // Only syntax errors + Standard, // Syntax + Unity best practices + Comprehensive, // All checks + semantic analysis + Strict // Treat all issues as errors + } + + /// + /// Validates basic code structure (braces, quotes, comments) + /// + private static bool ValidateBasicStructure(string contents, System.Collections.Generic.List errors) + { + bool isValid = true; + int braceBalance = 0; + int parenBalance = 0; + int bracketBalance = 0; + bool inStringLiteral = false; + bool inCharLiteral = false; + bool inSingleLineComment = false; + bool inMultiLineComment = false; + bool escaped = false; + + for (int i = 0; i < contents.Length; i++) + { + char c = contents[i]; + char next = i + 1 < contents.Length ? contents[i + 1] : '\0'; + + // Handle escape sequences + if (escaped) + { + escaped = false; + continue; + } + + if (c == '\\' && (inStringLiteral || inCharLiteral)) + { + escaped = true; + continue; + } + + // Handle comments + if (!inStringLiteral && !inCharLiteral) + { + if (c == '/' && next == '/' && !inMultiLineComment) + { + inSingleLineComment = true; + continue; + } + if (c == '/' && next == '*' && !inSingleLineComment) + { + inMultiLineComment = true; + i++; // Skip next character + continue; + } + if (c == '*' && next == '/' && inMultiLineComment) + { + inMultiLineComment = false; + i++; // Skip next character + continue; + } + } + + if (c == '\n') + { + inSingleLineComment = false; + continue; + } + + if (inSingleLineComment || inMultiLineComment) + continue; + + // Handle string and character literals + if (c == '"' && !inCharLiteral) + { + inStringLiteral = !inStringLiteral; + continue; + } + if (c == '\'' && !inStringLiteral) + { + inCharLiteral = !inCharLiteral; + continue; + } + + if (inStringLiteral || inCharLiteral) + continue; + + // Count brackets and braces + switch (c) + { + case '{': braceBalance++; break; + case '}': braceBalance--; break; + case '(': parenBalance++; break; + case ')': parenBalance--; break; + case '[': bracketBalance++; break; + case ']': bracketBalance--; break; + } + + // Check for negative balances (closing without opening) + if (braceBalance < 0) + { + errors.Add("ERROR: Unmatched closing brace '}'"); + isValid = false; + } + if (parenBalance < 0) + { + errors.Add("ERROR: Unmatched closing parenthesis ')'"); + isValid = false; + } + if (bracketBalance < 0) + { + errors.Add("ERROR: Unmatched closing bracket ']'"); + isValid = false; + } + } + + // Check final balances + if (braceBalance != 0) + { + errors.Add($"ERROR: Unbalanced braces (difference: {braceBalance})"); + isValid = false; + } + if (parenBalance != 0) + { + errors.Add($"ERROR: Unbalanced parentheses (difference: {parenBalance})"); + isValid = false; + } + if (bracketBalance != 0) + { + errors.Add($"ERROR: Unbalanced brackets (difference: {bracketBalance})"); + isValid = false; + } + if (inStringLiteral) + { + errors.Add("ERROR: Unterminated string literal"); + isValid = false; + } + if (inCharLiteral) + { + errors.Add("ERROR: Unterminated character literal"); + isValid = false; + } + if (inMultiLineComment) + { + errors.Add("WARNING: Unterminated multi-line comment"); + } + + return isValid; + } + +#if USE_ROSLYN + /// + /// Cached compilation references for performance + /// + private static System.Collections.Generic.List _cachedReferences = null; + private static DateTime _cacheTime = DateTime.MinValue; + private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(5); + + /// + /// Validates syntax using Roslyn compiler services + /// + private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List errors) + { + try + { + var syntaxTree = CSharpSyntaxTree.ParseText(contents); + var diagnostics = syntaxTree.GetDiagnostics(); + + bool hasErrors = false; + foreach (var diagnostic in diagnostics) + { + string severity = diagnostic.Severity.ToString().ToUpper(); + string message = $"{severity}: {diagnostic.GetMessage()}"; + + if (diagnostic.Severity == DiagnosticSeverity.Error) + { + hasErrors = true; + } + + // Include warnings in comprehensive mode + if (level >= ValidationLevel.Standard || diagnostic.Severity == DiagnosticSeverity.Error) //Also use Standard for now + { + var location = diagnostic.Location.GetLineSpan(); + if (location.IsValid) + { + message += $" (Line {location.StartLinePosition.Line + 1})"; + } + errors.Add(message); + } + } + + return !hasErrors; + } + catch (Exception ex) + { + errors.Add($"ERROR: Roslyn validation failed: {ex.Message}"); + return false; + } + } + + /// + /// Validates script semantics using full compilation context to catch namespace, type, and method resolution errors + /// + private static bool ValidateScriptSemantics(string contents, System.Collections.Generic.List errors) + { + try + { + // Get compilation references with caching + var references = GetCompilationReferences(); + if (references == null || references.Count == 0) + { + errors.Add("WARNING: Could not load compilation references for semantic validation"); + return true; // Don't fail if we can't get references + } + + // Create syntax tree + var syntaxTree = CSharpSyntaxTree.ParseText(contents); + + // Create compilation with full context + var compilation = CSharpCompilation.Create( + "TempValidation", + new[] { syntaxTree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + ); + + // Get semantic diagnostics - this catches all the issues you mentioned! + var diagnostics = compilation.GetDiagnostics(); + + bool hasErrors = false; + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Severity == DiagnosticSeverity.Error) + { + hasErrors = true; + var location = diagnostic.Location.GetLineSpan(); + string locationInfo = location.IsValid ? + $" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : ""; + + // Include diagnostic ID for better error identification + string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : ""; + errors.Add($"ERROR: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}"); + } + else if (diagnostic.Severity == DiagnosticSeverity.Warning) + { + var location = diagnostic.Location.GetLineSpan(); + string locationInfo = location.IsValid ? + $" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : ""; + + string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : ""; + errors.Add($"WARNING: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}"); + } + } + + return !hasErrors; + } + catch (Exception ex) + { + errors.Add($"ERROR: Semantic validation failed: {ex.Message}"); + return false; + } + } + + /// + /// Gets compilation references with caching for performance + /// + private static System.Collections.Generic.List GetCompilationReferences() + { + // Check cache validity + if (_cachedReferences != null && DateTime.Now - _cacheTime < CacheExpiry) + { + return _cachedReferences; + } + + try + { + var references = new System.Collections.Generic.List(); + + // Core .NET assemblies + references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); // mscorlib/System.Private.CoreLib + references.Add(MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location)); // System.Linq + references.Add(MetadataReference.CreateFromFile(typeof(System.Collections.Generic.List<>).Assembly.Location)); // System.Collections + + // Unity assemblies + try + { + references.Add(MetadataReference.CreateFromFile(typeof(UnityEngine.Debug).Assembly.Location)); // UnityEngine + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load UnityEngine assembly: {ex.Message}"); + } + +#if UNITY_EDITOR + try + { + references.Add(MetadataReference.CreateFromFile(typeof(UnityEditor.Editor).Assembly.Location)); // UnityEditor + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load UnityEditor assembly: {ex.Message}"); + } + + // Get Unity project assemblies + try + { + var assemblies = CompilationPipeline.GetAssemblies(); + foreach (var assembly in assemblies) + { + if (File.Exists(assembly.outputPath)) + { + references.Add(MetadataReference.CreateFromFile(assembly.outputPath)); + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load Unity project assemblies: {ex.Message}"); + } +#endif + + // Cache the results + _cachedReferences = references; + _cacheTime = DateTime.Now; + + return references; + } + catch (Exception ex) + { + Debug.LogError($"Failed to get compilation references: {ex.Message}"); + return new System.Collections.Generic.List(); + } + } +#else + private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List errors) + { + // Fallback when Roslyn is not available + return true; + } +#endif + + /// + /// Validates Unity-specific coding rules and best practices + /// //TODO: Naive Unity Checks and not really yield any results, need to be improved + /// + private static void ValidateScriptSyntaxUnity(string contents, System.Collections.Generic.List errors) + { + // Check for common Unity anti-patterns + if (contents.Contains("FindObjectOfType") && contents.Contains("Update()")) + { + errors.Add("WARNING: FindObjectOfType in Update() can cause performance issues"); + } + + if (contents.Contains("GameObject.Find") && contents.Contains("Update()")) + { + errors.Add("WARNING: GameObject.Find in Update() can cause performance issues"); + } + + // Check for proper MonoBehaviour usage + if (contents.Contains(": MonoBehaviour") && !contents.Contains("using UnityEngine")) + { + errors.Add("WARNING: MonoBehaviour requires 'using UnityEngine;'"); + } + + // Check for SerializeField usage + if (contents.Contains("[SerializeField]") && !contents.Contains("using UnityEngine")) + { + errors.Add("WARNING: SerializeField requires 'using UnityEngine;'"); + } + + // Check for proper coroutine usage + if (contents.Contains("StartCoroutine") && !contents.Contains("IEnumerator")) + { + errors.Add("WARNING: StartCoroutine typically requires IEnumerator methods"); + } + + // Check for Update without FixedUpdate for physics + if (contents.Contains("Rigidbody") && contents.Contains("Update()") && !contents.Contains("FixedUpdate()")) + { + errors.Add("WARNING: Consider using FixedUpdate() for Rigidbody operations"); + } + + // Check for missing null checks on Unity objects + if (contents.Contains("GetComponent<") && !contents.Contains("!= null")) + { + errors.Add("WARNING: Consider null checking GetComponent results"); + } + + // Check for proper event function signatures + if (contents.Contains("void Start(") && !contents.Contains("void Start()")) + { + errors.Add("WARNING: Start() should not have parameters"); + } + + if (contents.Contains("void Update(") && !contents.Contains("void Update()")) + { + errors.Add("WARNING: Update() should not have parameters"); + } + + // Check for inefficient string operations + if (contents.Contains("Update()") && contents.Contains("\"") && contents.Contains("+")) + { + errors.Add("WARNING: String concatenation in Update() can cause garbage collection issues"); + } + } + + /// + /// Validates semantic rules and common coding issues + /// + private static void ValidateSemanticRules(string contents, System.Collections.Generic.List errors) + { + // Check for potential memory leaks + if (contents.Contains("new ") && contents.Contains("Update()")) + { + errors.Add("WARNING: Creating objects in Update() may cause memory issues"); + } + + // Check for magic numbers + var magicNumberPattern = new Regex(@"\b\d+\.?\d*f?\b(?!\s*[;})\]])", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2)); + var matches = magicNumberPattern.Matches(contents); + if (matches.Count > 5) + { + errors.Add("WARNING: Consider using named constants instead of magic numbers"); + } + + // Check for long methods (simple line count check) + var methodPattern = new Regex(@"(public|private|protected|internal)?\s*(static)?\s*\w+\s+\w+\s*\([^)]*\)\s*{", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2)); + var methodMatches = methodPattern.Matches(contents); + foreach (Match match in methodMatches) + { + int startIndex = match.Index; + int braceCount = 0; + int lineCount = 0; + bool inMethod = false; + + for (int i = startIndex; i < contents.Length; i++) + { + if (contents[i] == '{') + { + braceCount++; + inMethod = true; + } + else if (contents[i] == '}') + { + braceCount--; + if (braceCount == 0 && inMethod) + break; + } + else if (contents[i] == '\n' && inMethod) + { + lineCount++; + } + } + + if (lineCount > 50) + { + errors.Add("WARNING: Method is very long, consider breaking it into smaller methods"); + break; // Only report once + } + } + + // Check for proper exception handling + if (contents.Contains("catch") && contents.Contains("catch()")) + { + errors.Add("WARNING: Empty catch blocks should be avoided"); + } + + // Check for proper async/await usage + if (contents.Contains("async ") && !contents.Contains("await")) + { + errors.Add("WARNING: Async method should contain await or return Task"); + } + + // Check for hardcoded tags and layers + if (contents.Contains("\"Player\"") || contents.Contains("\"Enemy\"")) + { + errors.Add("WARNING: Consider using constants for tags instead of hardcoded strings"); + } + } + + //TODO: A easier way for users to update incorrect scripts (now duplicated with the updateScript method and need to also update server side, put aside for now) + /// + /// Public method to validate script syntax with configurable validation level + /// Returns detailed validation results including errors and warnings + /// + // public static object ValidateScript(JObject @params) + // { + // string contents = @params["contents"]?.ToString(); + // string validationLevel = @params["validationLevel"]?.ToString() ?? "standard"; + + // if (string.IsNullOrEmpty(contents)) + // { + // return Response.Error("Contents parameter is required for validation."); + // } + + // // Parse validation level + // ValidationLevel level = ValidationLevel.Standard; + // switch (validationLevel.ToLower()) + // { + // case "basic": level = ValidationLevel.Basic; break; + // case "standard": level = ValidationLevel.Standard; break; + // case "comprehensive": level = ValidationLevel.Comprehensive; break; + // case "strict": level = ValidationLevel.Strict; break; + // default: + // return Response.Error($"Invalid validation level: '{validationLevel}'. Valid levels are: basic, standard, comprehensive, strict."); + // } + + // // Perform validation + // bool isValid = ValidateScriptSyntax(contents, level, out string[] validationErrors); + + // var errors = validationErrors?.Where(e => e.StartsWith("ERROR:")).ToArray() ?? new string[0]; + // var warnings = validationErrors?.Where(e => e.StartsWith("WARNING:")).ToArray() ?? new string[0]; + + // var result = new + // { + // isValid = isValid, + // validationLevel = validationLevel, + // errorCount = errors.Length, + // warningCount = warnings.Length, + // errors = errors, + // warnings = warnings, + // summary = isValid + // ? (warnings.Length > 0 ? $"Validation passed with {warnings.Length} warnings" : "Validation passed with no issues") + // : $"Validation failed with {errors.Length} errors and {warnings.Length} warnings" + // }; + + // if (isValid) + // { + // return Response.Success("Script validation completed successfully.", result); + // } + // else + // { + // return Response.Error("Script validation failed.", result); + // } + // } + } +} + +// Debounced refresh/compile scheduler to coalesce bursts of edits +static class RefreshDebounce +{ + private static int _pending; + private static readonly object _lock = new object(); + private static readonly HashSet _paths = new HashSet(StringComparer.OrdinalIgnoreCase); + + // The timestamp of the most recent schedule request. + private static DateTime _lastRequest; + + // Guard to ensure we only have a single ticking callback running. + private static bool _scheduled; + + public static void Schedule(string relPath, TimeSpan window) + { + // Record that work is pending and track the path in a threadsafe way. + Interlocked.Exchange(ref _pending, 1); + lock (_lock) + { + _paths.Add(relPath); + _lastRequest = DateTime.UtcNow; + + // If a debounce timer is already scheduled it will pick up the new request. + if (_scheduled) + return; + + _scheduled = true; + } + + // Kick off a ticking callback that waits until the window has elapsed + // from the last request before performing the refresh. + EditorApplication.delayCall += () => Tick(window); + // Nudge the editor loop so ticks run even if the window is unfocused + EditorApplication.QueuePlayerLoopUpdate(); + } + + private static void Tick(TimeSpan window) + { + bool ready; + lock (_lock) + { + // Only proceed once the debounce window has fully elapsed. + ready = (DateTime.UtcNow - _lastRequest) >= window; + if (ready) + { + _scheduled = false; + } + } + + if (!ready) + { + // Window has not yet elapsed; check again on the next editor tick. + EditorApplication.delayCall += () => Tick(window); + return; + } + + if (Interlocked.Exchange(ref _pending, 0) == 1) + { + string[] toImport; + lock (_lock) { toImport = _paths.ToArray(); _paths.Clear(); } + foreach (var p in toImport) + { + var sp = ManageScriptRefreshHelpers.SanitizeAssetsPath(p); + AssetDatabase.ImportAsset(sp, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport); + } +#if UNITY_EDITOR + UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); +#endif + // Fallback if needed: + // AssetDatabase.Refresh(); + } + } +} + +static class ManageScriptRefreshHelpers +{ + public static string SanitizeAssetsPath(string p) + { + if (string.IsNullOrEmpty(p)) return p; + p = p.Replace('\\', '/').Trim(); + if (p.StartsWith("unity://path/", StringComparison.OrdinalIgnoreCase)) + p = p.Substring("unity://path/".Length); + while (p.StartsWith("Assets/Assets/", StringComparison.OrdinalIgnoreCase)) + p = p.Substring("Assets/".Length); + if (!p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + p = "Assets/" + p.TrimStart('/'); + return p; + } + + public static void ScheduleScriptRefresh(string relPath) + { + var sp = SanitizeAssetsPath(relPath); + RefreshDebounce.Schedule(sp, TimeSpan.FromMilliseconds(200)); + } + + public static void ImportAndRequestCompile(string relPath, bool synchronous = true) + { + var sp = SanitizeAssetsPath(relPath); + var opts = ImportAssetOptions.ForceUpdate; + if (synchronous) opts |= ImportAssetOptions.ForceSynchronousImport; + AssetDatabase.ImportAsset(sp, opts); +#if UNITY_EDITOR + UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); +#endif + } +} diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs.meta b/MCPForUnity/Editor/Tools/ManageScript.cs.meta new file mode 100644 index 00000000..091cfe1c --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 626d2d44668019a45ae52e9ee066b7ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/ManageShader.cs b/MCPForUnity/Editor/Tools/ManageShader.cs new file mode 100644 index 00000000..2d7f4d0a --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageShader.cs @@ -0,0 +1,343 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Handles CRUD operations for shader files within the Unity project. + /// + [McpForUnityTool("manage_shader")] + public static class ManageShader + { + /// + /// Main handler for shader management actions. + /// + public static object HandleCommand(JObject @params) + { + // Extract parameters + string action = @params["action"]?.ToString().ToLower(); + string name = @params["name"]?.ToString(); + string path = @params["path"]?.ToString(); // Relative to Assets/ + string contents = null; + + // Check if we have base64 encoded contents + bool contentsEncoded = @params["contentsEncoded"]?.ToObject() ?? false; + if (contentsEncoded && @params["encodedContents"] != null) + { + try + { + contents = DecodeBase64(@params["encodedContents"].ToString()); + } + catch (Exception e) + { + return Response.Error($"Failed to decode shader contents: {e.Message}"); + } + } + else + { + contents = @params["contents"]?.ToString(); + } + + // Validate required parameters + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + if (string.IsNullOrEmpty(name)) + { + return Response.Error("Name parameter is required."); + } + // Basic name validation (alphanumeric, underscores, cannot start with number) + if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$")) + { + return Response.Error( + $"Invalid shader name: '{name}'. Use only letters, numbers, underscores, and don't start with a number." + ); + } + + // Ensure path is relative to Assets/, removing any leading "Assets/" + // Set default directory to "Shaders" if path is not provided + string relativeDir = path ?? "Shaders"; // Default to "Shaders" if path is null + if (!string.IsNullOrEmpty(relativeDir)) + { + relativeDir = relativeDir.Replace('\\', '/').Trim('/'); + if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/'); + } + } + // Handle empty string case explicitly after processing + if (string.IsNullOrEmpty(relativeDir)) + { + relativeDir = "Shaders"; // Ensure default if path was provided as "" or only "/" or "Assets/" + } + + // Construct paths + string shaderFileName = $"{name}.shader"; + string fullPathDir = Path.Combine(Application.dataPath, relativeDir); + string fullPath = Path.Combine(fullPathDir, shaderFileName); + string relativePath = Path.Combine("Assets", relativeDir, shaderFileName) + .Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes + + // Ensure the target directory exists for create/update + if (action == "create" || action == "update") + { + try + { + if (!Directory.Exists(fullPathDir)) + { + Directory.CreateDirectory(fullPathDir); + // Refresh AssetDatabase to recognize new folders + AssetDatabase.Refresh(); + } + } + catch (Exception e) + { + return Response.Error( + $"Could not create directory '{fullPathDir}': {e.Message}" + ); + } + } + + // Route to specific action handlers + switch (action) + { + case "create": + return CreateShader(fullPath, relativePath, name, contents); + case "read": + return ReadShader(fullPath, relativePath); + case "update": + return UpdateShader(fullPath, relativePath, name, contents); + case "delete": + return DeleteShader(fullPath, relativePath); + default: + return Response.Error( + $"Unknown action: '{action}'. Valid actions are: create, read, update, delete." + ); + } + } + + /// + /// Decode base64 string to normal text + /// + private static string DecodeBase64(string encoded) + { + byte[] data = Convert.FromBase64String(encoded); + return System.Text.Encoding.UTF8.GetString(data); + } + + /// + /// Encode text to base64 string + /// + private static string EncodeBase64(string text) + { + byte[] data = System.Text.Encoding.UTF8.GetBytes(text); + return Convert.ToBase64String(data); + } + + private static object CreateShader( + string fullPath, + string relativePath, + string name, + string contents + ) + { + // Check if shader already exists + if (File.Exists(fullPath)) + { + return Response.Error( + $"Shader already exists at '{relativePath}'. Use 'update' action to modify." + ); + } + + // Add validation for shader name conflicts in Unity + if (Shader.Find(name) != null) + { + return Response.Error( + $"A shader with name '{name}' already exists in the project. Choose a different name." + ); + } + + // Generate default content if none provided + if (string.IsNullOrEmpty(contents)) + { + contents = GenerateDefaultShaderContent(name); + } + + try + { + File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); + AssetDatabase.ImportAsset(relativePath); + AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader + return Response.Success( + $"Shader '{name}.shader' created successfully at '{relativePath}'.", + new { path = relativePath } + ); + } + catch (Exception e) + { + return Response.Error($"Failed to create shader '{relativePath}': {e.Message}"); + } + } + + private static object ReadShader(string fullPath, string relativePath) + { + if (!File.Exists(fullPath)) + { + return Response.Error($"Shader not found at '{relativePath}'."); + } + + try + { + string contents = File.ReadAllText(fullPath); + + // Return both normal and encoded contents for larger files + //TODO: Consider a threshold for large files + bool isLarge = contents.Length > 10000; // If content is large, include encoded version + var responseData = new + { + path = relativePath, + contents = contents, + // For large files, also include base64-encoded version + encodedContents = isLarge ? EncodeBase64(contents) : null, + contentsEncoded = isLarge, + }; + + return Response.Success( + $"Shader '{Path.GetFileName(relativePath)}' read successfully.", + responseData + ); + } + catch (Exception e) + { + return Response.Error($"Failed to read shader '{relativePath}': {e.Message}"); + } + } + + private static object UpdateShader( + string fullPath, + string relativePath, + string name, + string contents + ) + { + if (!File.Exists(fullPath)) + { + return Response.Error( + $"Shader not found at '{relativePath}'. Use 'create' action to add a new shader." + ); + } + if (string.IsNullOrEmpty(contents)) + { + return Response.Error("Content is required for the 'update' action."); + } + + try + { + File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); + AssetDatabase.ImportAsset(relativePath); + AssetDatabase.Refresh(); + return Response.Success( + $"Shader '{Path.GetFileName(relativePath)}' updated successfully.", + new { path = relativePath } + ); + } + catch (Exception e) + { + return Response.Error($"Failed to update shader '{relativePath}': {e.Message}"); + } + } + + private static object DeleteShader(string fullPath, string relativePath) + { + if (!File.Exists(fullPath)) + { + return Response.Error($"Shader not found at '{relativePath}'."); + } + + try + { + // Delete the asset through Unity's AssetDatabase first + bool success = AssetDatabase.DeleteAsset(relativePath); + if (!success) + { + return Response.Error($"Failed to delete shader through Unity's AssetDatabase: '{relativePath}'"); + } + + // If the file still exists (rare case), try direct deletion + if (File.Exists(fullPath)) + { + File.Delete(fullPath); + } + + return Response.Success($"Shader '{Path.GetFileName(relativePath)}' deleted successfully."); + } + catch (Exception e) + { + return Response.Error($"Failed to delete shader '{relativePath}': {e.Message}"); + } + } + + //This is a CGProgram template + //TODO: making a HLSL template as well? + private static string GenerateDefaultShaderContent(string name) + { + return @"Shader """ + name + @""" + { + Properties + { + _MainTex (""Texture"", 2D) = ""white"" {} + } + SubShader + { + Tags { ""RenderType""=""Opaque"" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #include ""UnityCG.cginc"" + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + float4 _MainTex_ST; + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + return o; + } + + fixed4 frag (v2f i) : SV_Target + { + fixed4 col = tex2D(_MainTex, i.uv); + return col; + } + ENDCG + } + } + }"; + } + } +} diff --git a/MCPForUnity/Editor/Tools/ManageShader.cs.meta b/MCPForUnity/Editor/Tools/ManageShader.cs.meta new file mode 100644 index 00000000..89d10cdd --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageShader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bcf4f1f3110494344b2af9324cf5c571 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs new file mode 100644 index 00000000..bb4e0431 --- /dev/null +++ b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs @@ -0,0 +1,37 @@ +using System; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Marks a class as an MCP tool handler for auto-discovery. + /// The class must have a public static HandleCommand(JObject) method. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class McpForUnityToolAttribute : Attribute + { + /// + /// The command name used to route requests to this tool. + /// If not specified, defaults to the PascalCase class name converted to snake_case. + /// + public string CommandName { get; } + + /// + /// Create an MCP tool attribute with auto-generated command name. + /// The command name will be derived from the class name (PascalCase → snake_case). + /// Example: ManageAsset → manage_asset + /// + public McpForUnityToolAttribute() + { + CommandName = null; // Will be auto-generated + } + + /// + /// Create an MCP tool attribute with explicit command name. + /// + /// The command name (e.g., "manage_asset") + public McpForUnityToolAttribute(string commandName) + { + CommandName = commandName; + } + } +} diff --git a/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs.meta b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs.meta new file mode 100644 index 00000000..57242c17 --- /dev/null +++ b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 804d07b886f4e4eb39316bbef34687c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/MenuItems.meta b/MCPForUnity/Editor/Tools/MenuItems.meta new file mode 100644 index 00000000..ffbda8e7 --- /dev/null +++ b/MCPForUnity/Editor/Tools/MenuItems.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2df8f144c6e684ec3bfd53e4a48f06ee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs b/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs new file mode 100644 index 00000000..e4b7eaf7 --- /dev/null +++ b/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs @@ -0,0 +1,42 @@ +using System; +using Newtonsoft.Json.Linq; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Tools.MenuItems +{ + [McpForUnityTool("manage_menu_item")] + public static class ManageMenuItem + { + /// + /// Routes actions: execute, list, exists, refresh + /// + public static object HandleCommand(JObject @params) + { + string action = @params["action"]?.ToString()?.ToLowerInvariant(); + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required. Valid actions are: execute, list, exists, refresh."); + } + + try + { + switch (action) + { + case "execute": + return MenuItemExecutor.Execute(@params); + case "list": + return MenuItemsReader.List(@params); + case "exists": + return MenuItemsReader.Exists(@params); + default: + return Response.Error($"Unknown action: '{action}'. Valid actions are: execute, list, exists, refresh."); + } + } + catch (Exception e) + { + McpLog.Error($"[ManageMenuItem] Action '{action}' failed: {e}"); + return Response.Error($"Internal error: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs.meta b/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs.meta new file mode 100644 index 00000000..aba1f496 --- /dev/null +++ b/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 77808278b21a6474a90f3abb91483f71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs b/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs new file mode 100644 index 00000000..193a80f6 --- /dev/null +++ b/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using UnityEditor; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Tools.MenuItems +{ + /// + /// Executes Unity Editor menu items by path with safety checks. + /// + public static class MenuItemExecutor + { + // Basic blacklist to prevent execution of disruptive menu items. + private static readonly HashSet _menuPathBlacklist = new HashSet( + StringComparer.OrdinalIgnoreCase) + { + "File/Quit", + }; + + /// + /// Execute a specific menu item. Expects 'menu_path' or 'menuPath' in params. + /// + public static object Execute(JObject @params) + { + string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString(); + if (string.IsNullOrWhiteSpace(menuPath)) + { + return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty."); + } + + if (_menuPathBlacklist.Contains(menuPath)) + { + return Response.Error($"Execution of menu item '{menuPath}' is blocked for safety reasons."); + } + + try + { + bool executed = EditorApplication.ExecuteMenuItem(menuPath); + if (!executed) + { + McpLog.Error($"[MenuItemExecutor] Failed to execute menu item '{menuPath}'. It might be invalid, disabled, or context-dependent."); + return Response.Error($"Failed to execute menu item '{menuPath}'. It might be invalid, disabled, or context-dependent."); + } + return Response.Success($"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors."); + } + catch (Exception e) + { + McpLog.Error($"[MenuItemExecutor] Failed to setup execution for '{menuPath}': {e}"); + return Response.Error($"Error setting up execution for menu item '{menuPath}': {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta b/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta new file mode 100644 index 00000000..2e9f4223 --- /dev/null +++ b/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ccc7c6ff549542e1ae4ba3463ae79d2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs b/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs new file mode 100644 index 00000000..60c94125 --- /dev/null +++ b/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using UnityEditor; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Tools.MenuItems +{ + /// + /// Provides read/list/exists capabilities for Unity menu items with caching. + /// + public static class MenuItemsReader + { + private static List _cached; + + [InitializeOnLoadMethod] + private static void Build() => Refresh(); + + /// + /// Returns the cached list, refreshing if necessary. + /// + public static IReadOnlyList AllMenuItems() => _cached ??= Refresh(); + + /// + /// Rebuilds the cached list from reflection. + /// + private static List Refresh() + { + try + { + var methods = TypeCache.GetMethodsWithAttribute(); + _cached = methods + // Methods can have multiple [MenuItem] attributes; collect them all + .SelectMany(m => m + .GetCustomAttributes(typeof(MenuItem), false) + .OfType() + .Select(attr => attr.menuItem)) + .Where(s => !string.IsNullOrEmpty(s)) + .Distinct(StringComparer.Ordinal) // Ensure no duplicates + .OrderBy(s => s, StringComparer.Ordinal) // Ensure consistent ordering + .ToList(); + return _cached; + } + catch (Exception e) + { + McpLog.Error($"[MenuItemsReader] Failed to scan menu items: {e}"); + _cached = _cached ?? new List(); + return _cached; + } + } + + /// + /// Returns a list of menu items. Optional 'search' param filters results. + /// + public static object List(JObject @params) + { + string search = @params["search"]?.ToString(); + bool doRefresh = @params["refresh"]?.ToObject() ?? false; + if (doRefresh || _cached == null) + { + Refresh(); + } + + IEnumerable result = _cached ?? Enumerable.Empty(); + if (!string.IsNullOrEmpty(search)) + { + result = result.Where(s => s.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0); + } + + return Response.Success("Menu items retrieved.", result.ToList()); + } + + /// + /// Checks if a given menu path exists in the cache. + /// + public static object Exists(JObject @params) + { + string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString(); + if (string.IsNullOrWhiteSpace(menuPath)) + { + return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty."); + } + + bool doRefresh = @params["refresh"]?.ToObject() ?? false; + if (doRefresh || _cached == null) + { + Refresh(); + } + + bool exists = (_cached ?? new List()).Contains(menuPath); + return Response.Success($"Exists check completed for '{menuPath}'.", new { exists }); + } + } +} diff --git a/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs.meta b/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs.meta new file mode 100644 index 00000000..78fd7ab4 --- /dev/null +++ b/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 37f212f83e8854ed7b5454d3733e4bfa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/Prefabs.meta b/MCPForUnity/Editor/Tools/Prefabs.meta new file mode 100644 index 00000000..4fb95c50 --- /dev/null +++ b/MCPForUnity/Editor/Tools/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1bd48a1b7555c46bba168078ce0291cc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs new file mode 100644 index 00000000..9e68d20e --- /dev/null +++ b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs @@ -0,0 +1,275 @@ +using System; +using System.IO; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace MCPForUnity.Editor.Tools.Prefabs +{ + [McpForUnityTool("manage_prefabs")] + public static class ManagePrefabs + { + private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject"; + + public static object HandleCommand(JObject @params) + { + if (@params == null) + { + return Response.Error("Parameters cannot be null."); + } + + string action = @params["action"]?.ToString()?.ToLowerInvariant(); + if (string.IsNullOrEmpty(action)) + { + return Response.Error($"Action parameter is required. Valid actions are: {SupportedActions}."); + } + + try + { + switch (action) + { + case "open_stage": + return OpenStage(@params); + case "close_stage": + return CloseStage(@params); + case "save_open_stage": + return SaveOpenStage(); + case "create_from_gameobject": + return CreatePrefabFromGameObject(@params); + default: + return Response.Error($"Unknown action: '{action}'. Valid actions are: {SupportedActions}."); + } + } + catch (Exception e) + { + McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}"); + return Response.Error($"Internal error: {e.Message}"); + } + } + + private static object OpenStage(JObject @params) + { + string prefabPath = @params["prefabPath"]?.ToString(); + if (string.IsNullOrEmpty(prefabPath)) + { + return Response.Error("'prefabPath' parameter is required for open_stage."); + } + + string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath); + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(sanitizedPath); + if (prefabAsset == null) + { + return Response.Error($"No prefab asset found at path '{sanitizedPath}'."); + } + + string modeValue = @params["mode"]?.ToString(); + if (!string.IsNullOrEmpty(modeValue) && !modeValue.Equals(PrefabStage.Mode.InIsolation.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return Response.Error("Only PrefabStage mode 'InIsolation' is supported at this time."); + } + + PrefabStage stage = PrefabStageUtility.OpenPrefab(sanitizedPath); + if (stage == null) + { + return Response.Error($"Failed to open prefab stage for '{sanitizedPath}'."); + } + + return Response.Success($"Opened prefab stage for '{sanitizedPath}'.", SerializeStage(stage)); + } + + private static object CloseStage(JObject @params) + { + PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); + if (stage == null) + { + return Response.Success("No prefab stage was open."); + } + + bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject() ?? false; + if (saveBeforeClose && stage.scene.isDirty) + { + SaveStagePrefab(stage); + AssetDatabase.SaveAssets(); + } + + StageUtility.GoToMainStage(); + return Response.Success($"Closed prefab stage for '{stage.assetPath}'."); + } + + private static object SaveOpenStage() + { + PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); + if (stage == null) + { + return Response.Error("No prefab stage is currently open."); + } + + SaveStagePrefab(stage); + AssetDatabase.SaveAssets(); + return Response.Success($"Saved prefab stage for '{stage.assetPath}'.", SerializeStage(stage)); + } + + private static void SaveStagePrefab(PrefabStage stage) + { + if (stage?.prefabContentsRoot == null) + { + throw new InvalidOperationException("Cannot save prefab stage without a prefab root."); + } + + bool saved = PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath); + if (!saved) + { + throw new InvalidOperationException($"Failed to save prefab asset at '{stage.assetPath}'."); + } + } + + private static object CreatePrefabFromGameObject(JObject @params) + { + string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString(); + if (string.IsNullOrEmpty(targetName)) + { + return Response.Error("'target' parameter is required for create_from_gameobject."); + } + + bool includeInactive = @params["searchInactive"]?.ToObject() ?? false; + GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive); + if (sourceObject == null) + { + return Response.Error($"GameObject '{targetName}' not found in the active scene."); + } + + if (PrefabUtility.IsPartOfPrefabAsset(sourceObject)) + { + return Response.Error( + $"GameObject '{sourceObject.name}' is part of a prefab asset. Open the prefab stage to save changes instead." + ); + } + + PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject); + if (status != PrefabInstanceStatus.NotAPrefab) + { + return Response.Error( + $"GameObject '{sourceObject.name}' is already linked to an existing prefab instance." + ); + } + + string requestedPath = @params["prefabPath"]?.ToString(); + if (string.IsNullOrWhiteSpace(requestedPath)) + { + return Response.Error("'prefabPath' parameter is required for create_from_gameobject."); + } + + string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath); + if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) + { + sanitizedPath += ".prefab"; + } + + bool allowOverwrite = @params["allowOverwrite"]?.ToObject() ?? false; + string finalPath = sanitizedPath; + + if (!allowOverwrite && AssetDatabase.LoadAssetAtPath(finalPath) != null) + { + finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath); + } + + EnsureAssetDirectoryExists(finalPath); + + try + { + GameObject connectedInstance = PrefabUtility.SaveAsPrefabAssetAndConnect( + sourceObject, + finalPath, + InteractionMode.AutomatedAction + ); + + if (connectedInstance == null) + { + return Response.Error($"Failed to save prefab asset at '{finalPath}'."); + } + + Selection.activeGameObject = connectedInstance; + + return Response.Success( + $"Prefab created at '{finalPath}' and instance linked.", + new + { + prefabPath = finalPath, + instanceId = connectedInstance.GetInstanceID() + } + ); + } + catch (Exception e) + { + return Response.Error($"Error saving prefab asset at '{finalPath}': {e.Message}"); + } + } + + private static void EnsureAssetDirectoryExists(string assetPath) + { + string directory = Path.GetDirectoryName(assetPath); + if (string.IsNullOrEmpty(directory)) + { + return; + } + + string fullDirectory = Path.Combine(Directory.GetCurrentDirectory(), directory); + if (!Directory.Exists(fullDirectory)) + { + Directory.CreateDirectory(fullDirectory); + AssetDatabase.Refresh(); + } + } + + private static GameObject FindSceneObjectByName(string name, bool includeInactive) + { + PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); + if (stage?.prefabContentsRoot != null) + { + foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren(includeInactive)) + { + if (transform.name == name) + { + return transform.gameObject; + } + } + } + + Scene activeScene = SceneManager.GetActiveScene(); + foreach (GameObject root in activeScene.GetRootGameObjects()) + { + foreach (Transform transform in root.GetComponentsInChildren(includeInactive)) + { + GameObject candidate = transform.gameObject; + if (candidate.name == name) + { + return candidate; + } + } + } + + return null; + } + + private static object SerializeStage(PrefabStage stage) + { + if (stage == null) + { + return new { isOpen = false }; + } + + return new + { + isOpen = true, + assetPath = stage.assetPath, + prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null, + mode = stage.mode.ToString(), + isDirty = stage.scene.isDirty + }; + } + + } +} diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs.meta b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs.meta new file mode 100644 index 00000000..27182e77 --- /dev/null +++ b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c14e76b2aa7bb4570a88903b061e946e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs b/MCPForUnity/Editor/Tools/ReadConsole.cs new file mode 100644 index 00000000..e94e5d51 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs @@ -0,0 +1,575 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; +using MCPForUnity.Editor.Helpers; // For Response class + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Handles reading and clearing Unity Editor console log entries. + /// Uses reflection to access internal LogEntry methods/properties. + /// + [McpForUnityTool("read_console")] + public static class ReadConsole + { + // (Calibration removed) + + // Reflection members for accessing internal LogEntry data + // private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection + private static MethodInfo _startGettingEntriesMethod; + private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End... + private static MethodInfo _clearMethod; + private static MethodInfo _getCountMethod; + private static MethodInfo _getEntryMethod; + private static FieldInfo _modeField; + private static FieldInfo _messageField; + private static FieldInfo _fileField; + private static FieldInfo _lineField; + private static FieldInfo _instanceIdField; + + // Note: Timestamp is not directly available in LogEntry; need to parse message or find alternative? + + // Static constructor for reflection setup + static ReadConsole() + { + try + { + Type logEntriesType = typeof(EditorApplication).Assembly.GetType( + "UnityEditor.LogEntries" + ); + if (logEntriesType == null) + throw new Exception("Could not find internal type UnityEditor.LogEntries"); + + + + // Include NonPublic binding flags as internal APIs might change accessibility + BindingFlags staticFlags = + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + BindingFlags instanceFlags = + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + _startGettingEntriesMethod = logEntriesType.GetMethod( + "StartGettingEntries", + staticFlags + ); + if (_startGettingEntriesMethod == null) + throw new Exception("Failed to reflect LogEntries.StartGettingEntries"); + + // Try reflecting EndGettingEntries based on warning message + _endGettingEntriesMethod = logEntriesType.GetMethod( + "EndGettingEntries", + staticFlags + ); + if (_endGettingEntriesMethod == null) + throw new Exception("Failed to reflect LogEntries.EndGettingEntries"); + + _clearMethod = logEntriesType.GetMethod("Clear", staticFlags); + if (_clearMethod == null) + throw new Exception("Failed to reflect LogEntries.Clear"); + + _getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags); + if (_getCountMethod == null) + throw new Exception("Failed to reflect LogEntries.GetCount"); + + _getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags); + if (_getEntryMethod == null) + throw new Exception("Failed to reflect LogEntries.GetEntryInternal"); + + Type logEntryType = typeof(EditorApplication).Assembly.GetType( + "UnityEditor.LogEntry" + ); + if (logEntryType == null) + throw new Exception("Could not find internal type UnityEditor.LogEntry"); + + _modeField = logEntryType.GetField("mode", instanceFlags); + if (_modeField == null) + throw new Exception("Failed to reflect LogEntry.mode"); + + _messageField = logEntryType.GetField("message", instanceFlags); + if (_messageField == null) + throw new Exception("Failed to reflect LogEntry.message"); + + _fileField = logEntryType.GetField("file", instanceFlags); + if (_fileField == null) + throw new Exception("Failed to reflect LogEntry.file"); + + _lineField = logEntryType.GetField("line", instanceFlags); + if (_lineField == null) + throw new Exception("Failed to reflect LogEntry.line"); + + _instanceIdField = logEntryType.GetField("instanceID", instanceFlags); + if (_instanceIdField == null) + throw new Exception("Failed to reflect LogEntry.instanceID"); + + // (Calibration removed) + + } + catch (Exception e) + { + Debug.LogError( + $"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}" + ); + // Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this. + _startGettingEntriesMethod = + _endGettingEntriesMethod = + _clearMethod = + _getCountMethod = + _getEntryMethod = + null; + _modeField = _messageField = _fileField = _lineField = _instanceIdField = null; + } + } + + // --- Main Handler --- + + public static object HandleCommand(JObject @params) + { + // Check if ALL required reflection members were successfully initialized. + if ( + _startGettingEntriesMethod == null + || _endGettingEntriesMethod == null + || _clearMethod == null + || _getCountMethod == null + || _getEntryMethod == null + || _modeField == null + || _messageField == null + || _fileField == null + || _lineField == null + || _instanceIdField == null + ) + { + // Log the error here as well for easier debugging in Unity Console + Debug.LogError( + "[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue." + ); + return Response.Error( + "ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs." + ); + } + + string action = @params["action"]?.ToString().ToLower() ?? "get"; + + try + { + if (action == "clear") + { + return ClearConsole(); + } + else if (action == "get") + { + // Extract parameters for 'get' + var types = + (@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList() + ?? new List { "error", "warning", "log" }; + int? count = @params["count"]?.ToObject(); + string filterText = @params["filterText"]?.ToString(); + string sinceTimestampStr = @params["sinceTimestamp"]?.ToString(); // TODO: Implement timestamp filtering + string format = (@params["format"]?.ToString() ?? "detailed").ToLower(); + bool includeStacktrace = + @params["includeStacktrace"]?.ToObject() ?? true; + + if (types.Contains("all")) + { + types = new List { "error", "warning", "log" }; // Expand 'all' + } + + if (!string.IsNullOrEmpty(sinceTimestampStr)) + { + Debug.LogWarning( + "[ReadConsole] Filtering by 'since_timestamp' is not currently implemented." + ); + // Need a way to get timestamp per log entry. + } + + return GetConsoleEntries(types, count, filterText, format, includeStacktrace); + } + else + { + return Response.Error( + $"Unknown action: '{action}'. Valid actions are 'get' or 'clear'." + ); + } + } + catch (Exception e) + { + Debug.LogError($"[ReadConsole] Action '{action}' failed: {e}"); + return Response.Error($"Internal error processing action '{action}': {e.Message}"); + } + } + + // --- Action Implementations --- + + private static object ClearConsole() + { + try + { + _clearMethod.Invoke(null, null); // Static method, no instance, no parameters + return Response.Success("Console cleared successfully."); + } + catch (Exception e) + { + Debug.LogError($"[ReadConsole] Failed to clear console: {e}"); + return Response.Error($"Failed to clear console: {e.Message}"); + } + } + + private static object GetConsoleEntries( + List types, + int? count, + string filterText, + string format, + bool includeStacktrace + ) + { + List formattedEntries = new List(); + int retrievedCount = 0; + + try + { + // LogEntries requires calling Start/Stop around GetEntries/GetEntryInternal + _startGettingEntriesMethod.Invoke(null, null); + + int totalEntries = (int)_getCountMethod.Invoke(null, null); + // Create instance to pass to GetEntryInternal - Ensure the type is correct + Type logEntryType = typeof(EditorApplication).Assembly.GetType( + "UnityEditor.LogEntry" + ); + if (logEntryType == null) + throw new Exception( + "Could not find internal type UnityEditor.LogEntry during GetConsoleEntries." + ); + object logEntryInstance = Activator.CreateInstance(logEntryType); + + for (int i = 0; i < totalEntries; i++) + { + // Get the entry data into our instance using reflection + _getEntryMethod.Invoke(null, new object[] { i, logEntryInstance }); + + // Extract data using reflection + int mode = (int)_modeField.GetValue(logEntryInstance); + string message = (string)_messageField.GetValue(logEntryInstance); + string file = (string)_fileField.GetValue(logEntryInstance); + + int line = (int)_lineField.GetValue(logEntryInstance); + // int instanceId = (int)_instanceIdField.GetValue(logEntryInstance); + + if (string.IsNullOrEmpty(message)) + { + continue; // Skip empty messages + } + + // (Calibration removed) + + // --- Filtering --- + // Prefer classifying severity from message/stacktrace; fallback to mode bits if needed + LogType unityType = InferTypeFromMessage(message); + bool isExplicitDebug = IsExplicitDebugLog(message); + if (!isExplicitDebug && unityType == LogType.Log) + { + unityType = GetLogTypeFromMode(mode); + } + + bool want; + // Treat Exception/Assert as errors for filtering convenience + if (unityType == LogType.Exception) + { + want = types.Contains("error") || types.Contains("exception"); + } + else if (unityType == LogType.Assert) + { + want = types.Contains("error") || types.Contains("assert"); + } + else + { + want = types.Contains(unityType.ToString().ToLowerInvariant()); + } + + if (!want) continue; + + // Filter by text (case-insensitive) + if ( + !string.IsNullOrEmpty(filterText) + && message.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) < 0 + ) + { + continue; + } + + // TODO: Filter by timestamp (requires timestamp data) + + // --- Formatting --- + string stackTrace = includeStacktrace ? ExtractStackTrace(message) : null; + // Always get first line for the message, use full message only if no stack trace exists + string[] messageLines = message.Split( + new[] { '\n', '\r' }, + StringSplitOptions.RemoveEmptyEntries + ); + string messageOnly = messageLines.Length > 0 ? messageLines[0] : message; + + // If not including stacktrace, ensure we only show the first line + if (!includeStacktrace) + { + stackTrace = null; + } + + object formattedEntry = null; + switch (format) + { + case "plain": + formattedEntry = messageOnly; + break; + case "json": + case "detailed": // Treat detailed as json for structured return + default: + formattedEntry = new + { + type = unityType.ToString(), + message = messageOnly, + file = file, + line = line, + // timestamp = "", // TODO + stackTrace = stackTrace, // Will be null if includeStacktrace is false or no stack found + }; + break; + } + + formattedEntries.Add(formattedEntry); + retrievedCount++; + + // Apply count limit (after filtering) + if (count.HasValue && retrievedCount >= count.Value) + { + break; + } + } + } + catch (Exception e) + { + Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}"); + // Ensure EndGettingEntries is called even if there's an error during iteration + try + { + _endGettingEntriesMethod.Invoke(null, null); + } + catch + { /* Ignore nested exception */ + } + return Response.Error($"Error retrieving log entries: {e.Message}"); + } + finally + { + // Ensure we always call EndGettingEntries + try + { + _endGettingEntriesMethod.Invoke(null, null); + } + catch (Exception e) + { + Debug.LogError($"[ReadConsole] Failed to call EndGettingEntries: {e}"); + // Don't return error here as we might have valid data, but log it. + } + } + + // Return the filtered and formatted list (might be empty) + return Response.Success( + $"Retrieved {formattedEntries.Count} log entries.", + formattedEntries + ); + } + + // --- Internal Helpers --- + + // Mapping bits from LogEntry.mode. These may vary by Unity version. + private const int ModeBitError = 1 << 0; + private const int ModeBitAssert = 1 << 1; + private const int ModeBitWarning = 1 << 2; + private const int ModeBitLog = 1 << 3; + private const int ModeBitException = 1 << 4; // often combined with Error bits + private const int ModeBitScriptingError = 1 << 9; + private const int ModeBitScriptingWarning = 1 << 10; + private const int ModeBitScriptingLog = 1 << 11; + private const int ModeBitScriptingException = 1 << 18; + private const int ModeBitScriptingAssertion = 1 << 22; + + private static LogType GetLogTypeFromMode(int mode) + { + // Preserve Unity's real type (no remapping); bits may vary by version + if ((mode & (ModeBitException | ModeBitScriptingException)) != 0) return LogType.Exception; + if ((mode & (ModeBitError | ModeBitScriptingError)) != 0) return LogType.Error; + if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) return LogType.Assert; + if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) return LogType.Warning; + return LogType.Log; + } + + // (Calibration helpers removed) + + /// + /// Classifies severity using message/stacktrace content. Works across Unity versions. + /// + private static LogType InferTypeFromMessage(string fullMessage) + { + if (string.IsNullOrEmpty(fullMessage)) return LogType.Log; + + // Fast path: look for explicit Debug API names in the appended stack trace + // e.g., "UnityEngine.Debug:LogError (object)" or "LogWarning" + if (fullMessage.IndexOf("LogError", StringComparison.OrdinalIgnoreCase) >= 0) + return LogType.Error; + if (fullMessage.IndexOf("LogWarning", StringComparison.OrdinalIgnoreCase) >= 0) + return LogType.Warning; + + // Compiler diagnostics (C#): "warning CSxxxx" / "error CSxxxx" + if (fullMessage.IndexOf(" warning CS", StringComparison.OrdinalIgnoreCase) >= 0 + || fullMessage.IndexOf(": warning CS", StringComparison.OrdinalIgnoreCase) >= 0) + return LogType.Warning; + if (fullMessage.IndexOf(" error CS", StringComparison.OrdinalIgnoreCase) >= 0 + || fullMessage.IndexOf(": error CS", StringComparison.OrdinalIgnoreCase) >= 0) + return LogType.Error; + + // Exceptions (avoid misclassifying compiler diagnostics) + if (fullMessage.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0) + return LogType.Exception; + + // Unity assertions + if (fullMessage.IndexOf("Assertion", StringComparison.OrdinalIgnoreCase) >= 0) + return LogType.Assert; + + return LogType.Log; + } + + private static bool IsExplicitDebugLog(string fullMessage) + { + if (string.IsNullOrEmpty(fullMessage)) return false; + if (fullMessage.IndexOf("Debug:Log (", StringComparison.OrdinalIgnoreCase) >= 0) return true; + if (fullMessage.IndexOf("UnityEngine.Debug:Log (", StringComparison.OrdinalIgnoreCase) >= 0) return true; + return false; + } + + /// + /// Applies the "one level lower" remapping for filtering, like the old version. + /// This ensures compatibility with the filtering logic that expects remapped types. + /// + private static LogType GetRemappedTypeForFiltering(LogType unityType) + { + switch (unityType) + { + case LogType.Error: + return LogType.Warning; // Error becomes Warning + case LogType.Warning: + return LogType.Log; // Warning becomes Log + case LogType.Assert: + return LogType.Assert; // Assert remains Assert + case LogType.Log: + return LogType.Log; // Log remains Log + case LogType.Exception: + return LogType.Warning; // Exception becomes Warning + default: + return LogType.Log; // Default fallback + } + } + + /// + /// Attempts to extract the stack trace part from a log message. + /// Unity log messages often have the stack trace appended after the main message, + /// starting on a new line and typically indented or beginning with "at ". + /// + /// The complete log message including potential stack trace. + /// The extracted stack trace string, or null if none is found. + private static string ExtractStackTrace(string fullMessage) + { + if (string.IsNullOrEmpty(fullMessage)) + return null; + + // Split into lines, removing empty ones to handle different line endings gracefully. + // Using StringSplitOptions.None might be better if empty lines matter within stack trace, but RemoveEmptyEntries is usually safer here. + string[] lines = fullMessage.Split( + new[] { '\r', '\n' }, + StringSplitOptions.RemoveEmptyEntries + ); + + // If there's only one line or less, there's no separate stack trace. + if (lines.Length <= 1) + return null; + + int stackStartIndex = -1; + + // Start checking from the second line onwards. + for (int i = 1; i < lines.Length; ++i) + { + // Performance: TrimStart creates a new string. Consider using IsWhiteSpace check if performance critical. + string trimmedLine = lines[i].TrimStart(); + + // Check for common stack trace patterns. + if ( + trimmedLine.StartsWith("at ") + || trimmedLine.StartsWith("UnityEngine.") + || trimmedLine.StartsWith("UnityEditor.") + || trimmedLine.Contains("(at ") + || // Covers "(at Assets/..." pattern + // Heuristic: Check if line starts with likely namespace/class pattern (Uppercase.Something) + ( + trimmedLine.Length > 0 + && char.IsUpper(trimmedLine[0]) + && trimmedLine.Contains('.') + ) + ) + { + stackStartIndex = i; + break; // Found the likely start of the stack trace + } + } + + // If a potential start index was found... + if (stackStartIndex > 0) + { + // Join the lines from the stack start index onwards using standard newline characters. + // This reconstructs the stack trace part of the message. + return string.Join("\n", lines.Skip(stackStartIndex)); + } + + // No clear stack trace found based on the patterns. + return null; + } + + /* LogEntry.mode bits exploration (based on Unity decompilation/observation): + May change between versions. + + Basic Types: + kError = 1 << 0 (1) + kAssert = 1 << 1 (2) + kWarning = 1 << 2 (4) + kLog = 1 << 3 (8) + kFatal = 1 << 4 (16) - Often treated as Exception/Error + + Modifiers/Context: + kAssetImportError = 1 << 7 (128) + kAssetImportWarning = 1 << 8 (256) + kScriptingError = 1 << 9 (512) + kScriptingWarning = 1 << 10 (1024) + kScriptingLog = 1 << 11 (2048) + kScriptCompileError = 1 << 12 (4096) + kScriptCompileWarning = 1 << 13 (8192) + kStickyError = 1 << 14 (16384) - Stays visible even after Clear On Play + kMayIgnoreLineNumber = 1 << 15 (32768) + kReportBug = 1 << 16 (65536) - Shows the "Report Bug" button + kDisplayPreviousErrorInStatusBar = 1 << 17 (131072) + kScriptingException = 1 << 18 (262144) + kDontExtractStacktrace = 1 << 19 (524288) - Hint to the console UI + kShouldClearOnPlay = 1 << 20 (1048576) - Default behavior + kGraphCompileError = 1 << 21 (2097152) + kScriptingAssertion = 1 << 22 (4194304) + kVisualScriptingError = 1 << 23 (8388608) + + Example observed values: + Log: 2048 (ScriptingLog) or 8 (Log) + Warning: 1028 (ScriptingWarning | Warning) or 4 (Warning) + Error: 513 (ScriptingError | Error) or 1 (Error) + Exception: 262161 (ScriptingException | Error | kFatal?) - Complex combination + Assertion: 4194306 (ScriptingAssertion | Assert) or 2 (Assert) + */ + } +} diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs.meta b/MCPForUnity/Editor/Tools/ReadConsole.cs.meta new file mode 100644 index 00000000..039895f8 --- /dev/null +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46c4f3614ed61f547ba823f0b2790267 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Windows.meta b/MCPForUnity/Editor/Windows.meta new file mode 100644 index 00000000..eda016e5 --- /dev/null +++ b/MCPForUnity/Editor/Windows.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d2ee39f5d4171184eb208e865c1ef4c1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs new file mode 100644 index 00000000..98a5295e --- /dev/null +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -0,0 +1,1670 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using System.Net.Sockets; +using System.Net; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Windows +{ + public class MCPForUnityEditorWindow : EditorWindow + { + private bool isUnityBridgeRunning = false; + private Vector2 scrollPosition; + private string pythonServerInstallationStatus = "Not Installed"; + private Color pythonServerInstallationStatusColor = Color.red; + private const int mcpPort = 6500; // MCP port (still hardcoded for MCP server) + private readonly McpClients mcpClients = new(); + private bool autoRegisterEnabled; + private bool lastClientRegisteredOk; + private bool lastBridgeVerifiedOk; + private string pythonDirOverride = null; + private bool debugLogsEnabled; + + // Script validation settings + private int validationLevelIndex = 1; // Default to Standard + private readonly string[] validationLevelOptions = new string[] + { + "Basic - Only syntax checks", + "Standard - Syntax + Unity practices", + "Comprehensive - All checks + semantic analysis", + "Strict - Full semantic validation (requires Roslyn)" + }; + + // UI state + private int selectedClientIndex = 0; + + [MenuItem("Window/MCP For Unity")] + public static void ShowWindow() + { + GetWindow("MCP For Unity"); + } + + private void OnEnable() + { + UpdatePythonServerInstallationStatus(); + + // Refresh bridge status + isUnityBridgeRunning = MCPForUnityBridge.IsRunning; + autoRegisterEnabled = EditorPrefs.GetBool("MCPForUnity.AutoRegisterEnabled", true); + debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); + if (debugLogsEnabled) + { + LogDebugPrefsState(); + } + foreach (McpClient mcpClient in mcpClients.clients) + { + CheckMcpConfiguration(mcpClient); + } + + // Load validation level setting + LoadValidationLevelSetting(); + + // First-run auto-setup only if Claude CLI is available + if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude())) + { + AutoFirstRunSetup(); + } + } + + private void OnFocus() + { + // Refresh bridge running state on focus in case initialization completed after domain reload + isUnityBridgeRunning = MCPForUnityBridge.IsRunning; + if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count) + { + McpClient selectedClient = mcpClients.clients[selectedClientIndex]; + CheckMcpConfiguration(selectedClient); + } + Repaint(); + } + + private Color GetStatusColor(McpStatus status) + { + // Return appropriate color based on the status enum + return status switch + { + McpStatus.Configured => Color.green, + McpStatus.Running => Color.green, + McpStatus.Connected => Color.green, + McpStatus.IncorrectPath => Color.yellow, + McpStatus.CommunicationError => Color.yellow, + McpStatus.NoResponse => Color.yellow, + _ => Color.red, // Default to red for error states or not configured + }; + } + + private void UpdatePythonServerInstallationStatus() + { + try + { + string installedPath = ServerInstaller.GetServerPath(); + bool installedOk = !string.IsNullOrEmpty(installedPath) && File.Exists(Path.Combine(installedPath, "server.py")); + if (installedOk) + { + pythonServerInstallationStatus = "Installed"; + pythonServerInstallationStatusColor = Color.green; + return; + } + + // Fall back to embedded/dev source via our existing resolution logic + string embeddedPath = FindPackagePythonDirectory(); + bool embeddedOk = !string.IsNullOrEmpty(embeddedPath) && File.Exists(Path.Combine(embeddedPath, "server.py")); + if (embeddedOk) + { + pythonServerInstallationStatus = "Installed (Embedded)"; + pythonServerInstallationStatusColor = Color.green; + } + else + { + pythonServerInstallationStatus = "Not Installed"; + pythonServerInstallationStatusColor = Color.red; + } + } + catch + { + pythonServerInstallationStatus = "Not Installed"; + pythonServerInstallationStatusColor = Color.red; + } + } + + + private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12) + { + float offsetX = (statusRect.width - size) / 2; + float offsetY = (statusRect.height - size) / 2; + Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size); + Vector3 center = new( + dotRect.x + (dotRect.width / 2), + dotRect.y + (dotRect.height / 2), + 0 + ); + float radius = size / 2; + + // Draw the main dot + Handles.color = statusColor; + Handles.DrawSolidDisc(center, Vector3.forward, radius); + + // Draw the border + Color borderColor = new( + statusColor.r * 0.7f, + statusColor.g * 0.7f, + statusColor.b * 0.7f + ); + Handles.color = borderColor; + Handles.DrawWireDisc(center, Vector3.forward, radius); + } + + private void OnGUI() + { + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + // Header + DrawHeader(); + + // Compute equal column widths for uniform layout + float horizontalSpacing = 2f; + float outerPadding = 20f; // approximate padding + // Make columns a bit less wide for a tighter layout + float computed = (position.width - outerPadding - horizontalSpacing) / 2f; + float colWidth = Mathf.Clamp(computed, 220f, 340f); + // Use fixed heights per row so paired panels match exactly + float topPanelHeight = 190f; + float bottomPanelHeight = 230f; + + // Top row: Server Status (left) and Unity Bridge (right) + EditorGUILayout.BeginHorizontal(); + { + EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight)); + DrawServerStatusSection(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(horizontalSpacing); + + EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight)); + DrawBridgeSection(); + EditorGUILayout.EndVertical(); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(10); + + // Second row: MCP Client Configuration (left) and Script Validation (right) + EditorGUILayout.BeginHorizontal(); + { + EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight)); + DrawUnifiedClientConfiguration(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(horizontalSpacing); + + EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight)); + DrawValidationSection(); + EditorGUILayout.EndVertical(); + } + EditorGUILayout.EndHorizontal(); + + // Minimal bottom padding + EditorGUILayout.Space(2); + + EditorGUILayout.EndScrollView(); + } + + private void DrawHeader() + { + EditorGUILayout.Space(15); + Rect titleRect = EditorGUILayout.GetControlRect(false, 40); + EditorGUI.DrawRect(titleRect, new Color(0.2f, 0.2f, 0.2f, 0.1f)); + + GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 16, + alignment = TextAnchor.MiddleLeft + }; + + GUI.Label( + new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height), + "MCP For Unity", + titleStyle + ); + + // Place the Show Debug Logs toggle on the same header row, right-aligned + float toggleWidth = 160f; + Rect toggleRect = new Rect(titleRect.xMax - toggleWidth - 12f, titleRect.y + 10f, toggleWidth, 20f); + bool newDebug = GUI.Toggle(toggleRect, debugLogsEnabled, "Show Debug Logs"); + if (newDebug != debugLogsEnabled) + { + debugLogsEnabled = newDebug; + EditorPrefs.SetBool("MCPForUnity.DebugLogs", debugLogsEnabled); + if (debugLogsEnabled) + { + LogDebugPrefsState(); + } + } + EditorGUILayout.Space(15); + } + + private void LogDebugPrefsState() + { + try + { + string pythonDirOverridePref = SafeGetPrefString("MCPForUnity.PythonDirOverride"); + string uvPathPref = SafeGetPrefString("MCPForUnity.UvPath"); + string serverSrcPref = SafeGetPrefString("MCPForUnity.ServerSrc"); + bool useEmbedded = SafeGetPrefBool("MCPForUnity.UseEmbeddedServer"); + + // Version-scoped detection key + string embeddedVer = ReadEmbeddedVersionOrFallback(); + string detectKey = $"MCPForUnity.LegacyDetectLogged:{embeddedVer}"; + bool detectLogged = SafeGetPrefBool(detectKey); + + // Project-scoped auto-register key + string projectPath = Application.dataPath ?? string.Empty; + string autoKey = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}"; + bool autoRegistered = SafeGetPrefBool(autoKey); + + MCPForUnity.Editor.Helpers.McpLog.Info( + "MCP Debug Prefs:\n" + + $" DebugLogs: {debugLogsEnabled}\n" + + $" PythonDirOverride: '{pythonDirOverridePref}'\n" + + $" UvPath: '{uvPathPref}'\n" + + $" ServerSrc: '{serverSrcPref}'\n" + + $" UseEmbeddedServer: {useEmbedded}\n" + + $" DetectOnceKey: '{detectKey}' => {detectLogged}\n" + + $" AutoRegisteredKey: '{autoKey}' => {autoRegistered}", + always: false + ); + } + catch (Exception ex) + { + UnityEngine.Debug.LogWarning($"MCP Debug Prefs logging failed: {ex.Message}"); + } + } + + private static string SafeGetPrefString(string key) + { + try { return EditorPrefs.GetString(key, string.Empty) ?? string.Empty; } catch { return string.Empty; } + } + + private static bool SafeGetPrefBool(string key) + { + try { return EditorPrefs.GetBool(key, false); } catch { return false; } + } + + private static string ReadEmbeddedVersionOrFallback() + { + try + { + if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc)) + { + var p = Path.Combine(embeddedSrc, "server_version.txt"); + if (File.Exists(p)) + { + var s = File.ReadAllText(p)?.Trim(); + if (!string.IsNullOrEmpty(s)) return s; + } + } + } + catch { } + return "unknown"; + } + + private void DrawServerStatusSection() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 14 + }; + EditorGUILayout.LabelField("Server Status", sectionTitleStyle); + EditorGUILayout.Space(8); + + EditorGUILayout.BeginHorizontal(); + Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); + DrawStatusDot(statusRect, pythonServerInstallationStatusColor, 16); + + GUIStyle statusStyle = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(pythonServerInstallationStatus, statusStyle, GUILayout.Height(28)); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + + EditorGUILayout.BeginHorizontal(); + bool isAutoMode = MCPForUnityBridge.IsAutoConnectMode(); + GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; + EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + int currentUnityPort = MCPForUnityBridge.GetCurrentPort(); + GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel) + { + fontSize = 11 + }; + EditorGUILayout.LabelField($"Ports: Unity {currentUnityPort}, MCP {mcpPort}", portStyle); + EditorGUILayout.Space(5); + + /// Auto-Setup button below ports + string setupButtonText = (lastClientRegisteredOk && lastBridgeVerifiedOk) ? "Connected ✓" : "Auto-Setup"; + if (GUILayout.Button(setupButtonText, GUILayout.Height(24))) + { + RunSetupNow(); + } + EditorGUILayout.Space(4); + + // Rebuild MCP Server button with tooltip tag + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + GUIContent repairLabel = new GUIContent( + "Rebuild MCP Server", + "Deletes the installed server and re-copies it from the package. Use this to update the server after making source code changes or if the installation is corrupted." + ); + if (GUILayout.Button(repairLabel, GUILayout.Width(160), GUILayout.Height(22))) + { + bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RebuildMcpServer(); + if (ok) + { + EditorUtility.DisplayDialog("MCP For Unity", "Server rebuilt successfully.", "OK"); + UpdatePythonServerInstallationStatus(); + } + else + { + EditorUtility.DisplayDialog("MCP For Unity", "Rebuild failed. Please check Console for details.", "OK"); + } + } + } + // (Removed descriptive tool tag under the Repair button) + + // (Show Debug Logs toggle moved to header) + EditorGUILayout.Space(2); + + // Python detection warning with link + if (!IsPythonDetected()) + { + GUIStyle warnStyle = new GUIStyle(EditorStyles.label) { richText = true, wordWrap = true }; + EditorGUILayout.LabelField("Warning: No Python installation found.", warnStyle); + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("Open Install Instructions", GUILayout.Width(200))) + { + Application.OpenURL("https://www.python.org/downloads/"); + } + } + EditorGUILayout.Space(4); + } + + // Troubleshooting helpers + if (pythonServerInstallationStatusColor != Color.green) + { + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("Select server folder…", GUILayout.Width(160))) + { + string picked = EditorUtility.OpenFolderPanel("Select UnityMcpServer/src", Application.dataPath, ""); + if (!string.IsNullOrEmpty(picked) && File.Exists(Path.Combine(picked, "server.py"))) + { + pythonDirOverride = picked; + EditorPrefs.SetString("MCPForUnity.PythonDirOverride", pythonDirOverride); + UpdatePythonServerInstallationStatus(); + } + else if (!string.IsNullOrEmpty(picked)) + { + EditorUtility.DisplayDialog("Invalid Selection", "The selected folder does not contain server.py", "OK"); + } + } + if (GUILayout.Button("Verify again", GUILayout.Width(120))) + { + UpdatePythonServerInstallationStatus(); + } + } + } + EditorGUILayout.EndVertical(); + } + + private void DrawBridgeSection() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Always reflect the live state each repaint to avoid stale UI after recompiles + isUnityBridgeRunning = MCPForUnityBridge.IsRunning; + + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 14 + }; + EditorGUILayout.LabelField("Unity Bridge", sectionTitleStyle); + EditorGUILayout.Space(8); + + EditorGUILayout.BeginHorizontal(); + Color bridgeColor = isUnityBridgeRunning ? Color.green : Color.red; + Rect bridgeStatusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); + DrawStatusDot(bridgeStatusRect, bridgeColor, 16); + + GUIStyle bridgeStatusStyle = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(isUnityBridgeRunning ? "Running" : "Stopped", bridgeStatusStyle, GUILayout.Height(28)); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge", GUILayout.Height(32))) + { + ToggleUnityBridge(); + } + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + } + + private void DrawValidationSection() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 14 + }; + EditorGUILayout.LabelField("Script Validation", sectionTitleStyle); + EditorGUILayout.Space(8); + + EditorGUI.BeginChangeCheck(); + validationLevelIndex = EditorGUILayout.Popup("Validation Level", validationLevelIndex, validationLevelOptions, GUILayout.Height(20)); + if (EditorGUI.EndChangeCheck()) + { + SaveValidationLevelSetting(); + } + + EditorGUILayout.Space(8); + string description = GetValidationLevelDescription(validationLevelIndex); + EditorGUILayout.HelpBox(description, MessageType.Info); + EditorGUILayout.Space(4); + // (Show Debug Logs toggle moved to header) + EditorGUILayout.Space(2); + EditorGUILayout.EndVertical(); + } + + private void DrawUnifiedClientConfiguration() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 14 + }; + EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle); + EditorGUILayout.Space(10); + + // (Auto-connect toggle removed per design) + + // Client selector + string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray(); + EditorGUI.BeginChangeCheck(); + selectedClientIndex = EditorGUILayout.Popup("Select Client", selectedClientIndex, clientNames, GUILayout.Height(20)); + if (EditorGUI.EndChangeCheck()) + { + selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1); + } + + EditorGUILayout.Space(10); + + if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count) + { + McpClient selectedClient = mcpClients.clients[selectedClientIndex]; + DrawClientConfigurationCompact(selectedClient); + } + + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + } + + private void AutoFirstRunSetup() + { + try + { + // Project-scoped one-time flag + string projectPath = Application.dataPath ?? string.Empty; + string key = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}"; + if (EditorPrefs.GetBool(key, false)) + { + return; + } + + // Attempt client registration using discovered Python server dir + pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null); + string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); + if (!string.IsNullOrEmpty(pythonDir) && File.Exists(Path.Combine(pythonDir, "server.py"))) + { + bool anyRegistered = false; + foreach (McpClient client in mcpClients.clients) + { + try + { + if (client.mcpType == McpTypes.ClaudeCode) + { + // Only attempt if Claude CLI is present + if (!IsClaudeConfigured() && !string.IsNullOrEmpty(ExecPath.ResolveClaude())) + { + RegisterWithClaudeCode(pythonDir); + anyRegistered = true; + } + } + else + { + CheckMcpConfiguration(client); + bool alreadyConfigured = client.status == McpStatus.Configured; + if (!alreadyConfigured) + { + ConfigureMcpClient(client); + anyRegistered = true; + } + } + } + catch (Exception ex) + { + MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}"); + } + } + lastClientRegisteredOk = anyRegistered + || IsCursorConfigured(pythonDir) + || CodexConfigHelper.IsCodexConfigured(pythonDir) + || IsClaudeConfigured(); + } + + // Ensure the bridge is listening and has a fresh saved port + if (!MCPForUnityBridge.IsRunning) + { + try + { + MCPForUnityBridge.StartAutoConnect(); + isUnityBridgeRunning = MCPForUnityBridge.IsRunning; + Repaint(); + } + catch (Exception ex) + { + MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup StartAutoConnect failed: {ex.Message}"); + } + } + + // Verify bridge with a quick ping + lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort()); + + EditorPrefs.SetBool(key, true); + } + catch (Exception e) + { + MCPForUnity.Editor.Helpers.McpLog.Warn($"MCP for Unity auto-setup skipped: {e.Message}"); + } + } + + private static string ComputeSha1(string input) + { + try + { + using SHA1 sha1 = SHA1.Create(); + byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty); + byte[] hash = sha1.ComputeHash(bytes); + StringBuilder sb = new StringBuilder(hash.Length * 2); + foreach (byte b in hash) + { + sb.Append(b.ToString("x2")); + } + return sb.ToString(); + } + catch + { + return ""; + } + } + + private void RunSetupNow() + { + // Force a one-shot setup regardless of first-run flag + try + { + pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null); + string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); + if (string.IsNullOrEmpty(pythonDir) || !File.Exists(Path.Combine(pythonDir, "server.py"))) + { + EditorUtility.DisplayDialog("Setup", "Python server not found. Please select UnityMcpServer/src.", "OK"); + return; + } + + bool anyRegistered = false; + foreach (McpClient client in mcpClients.clients) + { + try + { + if (client.mcpType == McpTypes.ClaudeCode) + { + if (!IsClaudeConfigured()) + { + RegisterWithClaudeCode(pythonDir); + anyRegistered = true; + } + } + else + { + CheckMcpConfiguration(client); + bool alreadyConfigured = client.status == McpStatus.Configured; + if (!alreadyConfigured) + { + ConfigureMcpClient(client); + anyRegistered = true; + } + } + } + catch (Exception ex) + { + UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}"); + } + } + lastClientRegisteredOk = anyRegistered + || IsCursorConfigured(pythonDir) + || CodexConfigHelper.IsCodexConfigured(pythonDir) + || IsClaudeConfigured(); + + // Restart/ensure bridge + MCPForUnityBridge.StartAutoConnect(); + isUnityBridgeRunning = MCPForUnityBridge.IsRunning; + + // Verify + lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort()); + Repaint(); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("Setup Failed", e.Message, "OK"); + } + } + + private static bool IsCursorConfigured(string pythonDir) + { + try + { + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cursor", "mcp.json") + : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cursor", "mcp.json"); + if (!File.Exists(configPath)) return false; + string json = File.ReadAllText(configPath); + dynamic cfg = JsonConvert.DeserializeObject(json); + var servers = cfg?.mcpServers; + if (servers == null) return false; + var unity = servers.unityMCP ?? servers.UnityMCP; + if (unity == null) return false; + var args = unity.args; + if (args == null) return false; + // Prefer exact extraction of the --directory value and compare normalized paths + string[] strArgs = ((System.Collections.Generic.IEnumerable)args) + .Select(x => x?.ToString() ?? string.Empty) + .ToArray(); + string dir = McpConfigFileHelper.ExtractDirectoryArg(strArgs); + if (string.IsNullOrEmpty(dir)) return false; + return McpConfigFileHelper.PathsEqual(dir, pythonDir); + } + catch { return false; } + } + + private static bool IsClaudeConfigured() + { + try + { + string claudePath = ExecPath.ResolveClaude(); + if (string.IsNullOrEmpty(claudePath)) return false; + + // Only prepend PATH on Unix + string pathPrepend = null; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + pathPrepend = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" + : "/usr/local/bin:/usr/bin:/bin"; + } + + if (!ExecPath.TryRun(claudePath, "mcp list", workingDir: null, out var stdout, out var stderr, 5000, pathPrepend)) + { + return false; + } + return (stdout ?? string.Empty).IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0; + } + catch { return false; } + } + + private static bool VerifyBridgePing(int port) + { + // Use strict framed protocol to match bridge (FRAMING=1) + const int ConnectTimeoutMs = 1000; + const int FrameTimeoutMs = 30000; // match bridge frame I/O timeout + + try + { + using TcpClient client = new TcpClient(); + var connectTask = client.ConnectAsync(IPAddress.Loopback, port); + if (!connectTask.Wait(ConnectTimeoutMs)) return false; + + using NetworkStream stream = client.GetStream(); + try { client.NoDelay = true; } catch { } + + // 1) Read handshake line (ASCII, newline-terminated) + string handshake = ReadLineAscii(stream, 2000); + if (string.IsNullOrEmpty(handshake) || handshake.IndexOf("FRAMING=1", StringComparison.OrdinalIgnoreCase) < 0) + { + UnityEngine.Debug.LogWarning("MCP for Unity: Bridge handshake missing FRAMING=1"); + return false; + } + + // 2) Send framed "ping" + byte[] payload = Encoding.UTF8.GetBytes("ping"); + WriteFrame(stream, payload, FrameTimeoutMs); + + // 3) Read framed response and check for pong + string response = ReadFrameUtf8(stream, FrameTimeoutMs); + bool ok = !string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0; + if (!ok) + { + UnityEngine.Debug.LogWarning($"MCP for Unity: Framed ping failed; response='{response}'"); + } + return ok; + } + catch (Exception ex) + { + UnityEngine.Debug.LogWarning($"MCP for Unity: VerifyBridgePing error: {ex.Message}"); + return false; + } + } + + // Minimal framing helpers (8-byte big-endian length prefix), blocking with timeouts + private static void WriteFrame(NetworkStream stream, byte[] payload, int timeoutMs) + { + if (payload == null) throw new ArgumentNullException(nameof(payload)); + if (payload.LongLength < 1) throw new IOException("Zero-length frames are not allowed"); + byte[] header = new byte[8]; + ulong len = (ulong)payload.LongLength; + header[0] = (byte)(len >> 56); + header[1] = (byte)(len >> 48); + header[2] = (byte)(len >> 40); + header[3] = (byte)(len >> 32); + header[4] = (byte)(len >> 24); + header[5] = (byte)(len >> 16); + header[6] = (byte)(len >> 8); + header[7] = (byte)(len); + + stream.WriteTimeout = timeoutMs; + stream.Write(header, 0, header.Length); + stream.Write(payload, 0, payload.Length); + } + + private static string ReadFrameUtf8(NetworkStream stream, int timeoutMs) + { + byte[] header = ReadExact(stream, 8, timeoutMs); + ulong len = ((ulong)header[0] << 56) + | ((ulong)header[1] << 48) + | ((ulong)header[2] << 40) + | ((ulong)header[3] << 32) + | ((ulong)header[4] << 24) + | ((ulong)header[5] << 16) + | ((ulong)header[6] << 8) + | header[7]; + if (len == 0UL) throw new IOException("Zero-length frames are not allowed"); + if (len > int.MaxValue) throw new IOException("Frame too large"); + byte[] payload = ReadExact(stream, (int)len, timeoutMs); + return Encoding.UTF8.GetString(payload); + } + + private static byte[] ReadExact(NetworkStream stream, int count, int timeoutMs) + { + byte[] buffer = new byte[count]; + int offset = 0; + stream.ReadTimeout = timeoutMs; + while (offset < count) + { + int read = stream.Read(buffer, offset, count - offset); + if (read <= 0) throw new IOException("Connection closed before reading expected bytes"); + offset += read; + } + return buffer; + } + + private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int maxLen = 512) + { + stream.ReadTimeout = timeoutMs; + using var ms = new MemoryStream(); + byte[] one = new byte[1]; + while (ms.Length < maxLen) + { + int n = stream.Read(one, 0, 1); + if (n <= 0) break; + if (one[0] == (byte)'\n') break; + ms.WriteByte(one[0]); + } + return Encoding.ASCII.GetString(ms.ToArray()); + } + + private void DrawClientConfigurationCompact(McpClient mcpClient) + { + // Special pre-check for Claude Code: if CLI missing, reflect in status UI + if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + string claudeCheck = ExecPath.ResolveClaude(); + if (string.IsNullOrEmpty(claudeCheck)) + { + mcpClient.configStatus = "Claude Not Found"; + mcpClient.status = McpStatus.NotConfigured; + } + } + + // Pre-check for clients that require uv (all except Claude Code) + bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode; + bool uvMissingEarly = false; + if (uvRequired) + { + string uvPathEarly = FindUvPath(); + if (string.IsNullOrEmpty(uvPathEarly)) + { + uvMissingEarly = true; + mcpClient.configStatus = "uv Not Found"; + mcpClient.status = McpStatus.NotConfigured; + } + } + + // Status display + EditorGUILayout.BeginHorizontal(); + Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); + Color statusColor = GetStatusColor(mcpClient.status); + DrawStatusDot(statusRect, statusColor, 16); + + GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28)); + EditorGUILayout.EndHorizontal(); + // When Claude CLI is missing, show a clear install hint directly below status + if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) + { + GUIStyle installHintStyle = new GUIStyle(clientStatusStyle); + installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange + EditorGUILayout.BeginHorizontal(); + GUIContent installText = new GUIContent("Make sure Claude Code is installed!"); + Vector2 textSize = installHintStyle.CalcSize(installText); + EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false)); + GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; + GUILayout.Space(6); + if (GUILayout.Button("[HELP]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code"); + } + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(10); + + // If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls + if (uvRequired && uvMissingEarly) + { + GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold, + wordWrap = false + }; + installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f); + EditorGUILayout.BeginHorizontal(); + GUIContent installText2 = new GUIContent("Make sure uv is installed!"); + Vector2 sz = installHintStyle2.CalcSize(installText2); + EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false)); + GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; + GUILayout.Space(6); + if (GUILayout.Button("[HELP]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf"); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Choose uv Install Location", GUILayout.Width(260), GUILayout.Height(22))) + { + string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, ""); + if (!string.IsNullOrEmpty(picked)) + { + EditorPrefs.SetString("MCPForUnity.UvPath", picked); + ConfigureMcpClient(mcpClient); + Repaint(); + } + } + EditorGUILayout.EndHorizontal(); + return; + } + + // Action buttons in horizontal layout + EditorGUILayout.BeginHorizontal(); + + if (mcpClient.mcpType == McpTypes.VSCode) + { + if (GUILayout.Button("Auto Configure", GUILayout.Height(32))) + { + ConfigureMcpClient(mcpClient); + } + } + else if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); + if (claudeAvailable) + { + bool isConfigured = mcpClient.status == McpStatus.Configured; + string buttonText = isConfigured ? "Unregister MCP for Unity with Claude Code" : "Register with Claude Code"; + if (GUILayout.Button(buttonText, GUILayout.Height(32))) + { + if (isConfigured) + { + UnregisterWithClaudeCode(); + } + else + { + string pythonDir = FindPackagePythonDirectory(); + RegisterWithClaudeCode(pythonDir); + } + } + // Hide the picker once a valid binary is available + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true }; + string resolvedClaude = ExecPath.ResolveClaude(); + EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + } + // CLI picker row (only when not found) + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + if (!claudeAvailable) + { + // Only show the picker button in not-found state (no redundant "not found" label) + if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22))) + { + string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, ""); + if (!string.IsNullOrEmpty(picked)) + { + ExecPath.SetClaudeCliPath(picked); + // Auto-register after setting a valid path + string pythonDir = FindPackagePythonDirectory(); + RegisterWithClaudeCode(pythonDir); + Repaint(); + } + } + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + } + else + { + if (GUILayout.Button($"Auto Configure", GUILayout.Height(32))) + { + ConfigureMcpClient(mcpClient); + } + } + + if (mcpClient.mcpType != McpTypes.ClaudeCode) + { + if (GUILayout.Button("Manual Setup", GUILayout.Height(32))) + { + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + + if (mcpClient.mcpType == McpTypes.VSCode) + { + string pythonDir = FindPackagePythonDirectory(); + string uvPath = FindUvPath(); + if (uvPath == null) + { + UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode."); + return; + } + // VSCode now reads from mcp.json with a top-level "servers" block + var vscodeConfig = new + { + servers = new + { + unityMCP = new + { + command = uvPath, + args = new[] { "run", "--directory", pythonDir, "server.py" } + } + } + }; + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); + } + else + { + ShowManualInstructionsWindow(configPath, mcpClient); + } + } + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + // Quick info (hide when Claude is not found to avoid confusion) + bool hideConfigInfo = + (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) + || ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath())); + if (!hideConfigInfo) + { + GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel) + { + fontSize = 10 + }; + EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle); + } + } + + private void ToggleUnityBridge() + { + if (isUnityBridgeRunning) + { + MCPForUnityBridge.Stop(); + } + else + { + MCPForUnityBridge.Start(); + } + // Reflect the actual state post-operation (avoid optimistic toggle) + isUnityBridgeRunning = MCPForUnityBridge.IsRunning; + Repaint(); + } + + // New method to show manual instructions without changing status + private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient) + { + // Get the Python directory path using Package Manager API + string pythonDir = FindPackagePythonDirectory(); + // Build manual JSON centrally using the shared builder + string uvPathForManual = FindUvPath(); + if (uvPathForManual == null) + { + UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration."); + return; + } + + string manualConfig = mcpClient?.mcpType == McpTypes.Codex + ? CodexConfigHelper.BuildCodexServerBlock(uvPathForManual, McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)).TrimEnd() + Environment.NewLine + : ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient); + ManualConfigEditorWindow.ShowWindow(configPath, manualConfig, mcpClient); + } + + private string FindPackagePythonDirectory() + { + // Use shared helper for consistent path resolution across both windows + return McpPathResolver.FindPackagePythonDirectory(debugLogsEnabled); + } + + private string ConfigureMcpClient(McpClient mcpClient) + { + try + { + // Use shared helper for consistent config path resolution + string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); + + // Create directory if it doesn't exist + McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); + + // Find the server.py file location using shared helper + string pythonDir = FindPackagePythonDirectory(); + + if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) + { + ShowManualInstructionsWindow(configPath, mcpClient); + return "Manual Configuration Required"; + } + + string result = mcpClient.mcpType == McpTypes.Codex + ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) + : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); + + // Update the client status after successful configuration + if (result == "Configured successfully") + { + mcpClient.SetStatus(McpStatus.Configured); + } + + return result; + } + catch (Exception e) + { + // Determine the config file path based on OS for error message + string configPath = ""; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + configPath = mcpClient.windowsConfigPath; + } + else if ( + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + ) + { + configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) + ? mcpClient.linuxConfigPath + : mcpClient.macConfigPath; + } + else if ( + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ) + { + configPath = mcpClient.linuxConfigPath; + } + + ShowManualInstructionsWindow(configPath, mcpClient); + UnityEngine.Debug.LogError( + $"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}" + ); + return $"Failed to configure {mcpClient.name}"; + } + } + + private void LoadValidationLevelSetting() + { + string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); + validationLevelIndex = savedLevel.ToLower() switch + { + "basic" => 0, + "standard" => 1, + "comprehensive" => 2, + "strict" => 3, + _ => 1 // Default to Standard + }; + } + + private void SaveValidationLevelSetting() + { + string levelString = validationLevelIndex switch + { + 0 => "basic", + 1 => "standard", + 2 => "comprehensive", + 3 => "strict", + _ => "standard" + }; + EditorPrefs.SetString("MCPForUnity_ScriptValidationLevel", levelString); + } + + private string GetValidationLevelDescription(int index) + { + return index switch + { + 0 => "Only basic syntax checks (braces, quotes, comments)", + 1 => "Syntax checks + Unity best practices and warnings", + 2 => "All checks + semantic analysis and performance warnings", + 3 => "Full semantic validation with namespace/type resolution (requires Roslyn)", + _ => "Standard validation" + }; + } + + private void CheckMcpConfiguration(McpClient mcpClient) + { + try + { + // Special handling for Claude Code + if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + CheckClaudeCodeConfiguration(mcpClient); + return; + } + + // Use shared helper for consistent config path resolution + string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); + + if (!File.Exists(configPath)) + { + mcpClient.SetStatus(McpStatus.NotConfigured); + return; + } + + string configJson = File.ReadAllText(configPath); + // Use the same path resolution as configuration to avoid false "Incorrect Path" in dev mode + string pythonDir = FindPackagePythonDirectory(); + + // Use switch statement to handle different client types, extracting common logic + string[] args = null; + bool configExists = false; + + switch (mcpClient.mcpType) + { + case McpTypes.VSCode: + dynamic config = JsonConvert.DeserializeObject(configJson); + + // New schema: top-level servers + if (config?.servers?.unityMCP != null) + { + args = config.servers.unityMCP.args.ToObject(); + configExists = true; + } + // Back-compat: legacy mcp.servers + else if (config?.mcp?.servers?.unityMCP != null) + { + args = config.mcp.servers.unityMCP.args.ToObject(); + configExists = true; + } + break; + + case McpTypes.Codex: + if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs)) + { + args = codexArgs; + configExists = true; + } + break; + + default: + // Standard MCP configuration check for Claude Desktop, Cursor, etc. + McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); + + if (standardConfig?.mcpServers?.unityMCP != null) + { + args = standardConfig.mcpServers.unityMCP.args; + configExists = true; + } + break; + } + + // Common logic for checking configuration status + if (configExists) + { + string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args); + bool matches = !string.IsNullOrEmpty(configuredDir) && McpConfigFileHelper.PathsEqual(configuredDir, pythonDir); + if (matches) + { + mcpClient.SetStatus(McpStatus.Configured); + } + else + { + // Attempt auto-rewrite once if the package path changed + try + { + string rewriteResult = mcpClient.mcpType == McpTypes.Codex + ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) + : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); + if (rewriteResult == "Configured successfully") + { + if (debugLogsEnabled) + { + MCPForUnity.Editor.Helpers.McpLog.Info($"Auto-updated MCP config for '{mcpClient.name}' to new path: {pythonDir}", always: false); + } + mcpClient.SetStatus(McpStatus.Configured); + } + else + { + mcpClient.SetStatus(McpStatus.IncorrectPath); + } + } + catch (Exception ex) + { + mcpClient.SetStatus(McpStatus.IncorrectPath); + if (debugLogsEnabled) + { + UnityEngine.Debug.LogWarning($"MCP for Unity: Auto-config rewrite failed for '{mcpClient.name}': {ex.Message}"); + } + } + } + } + else + { + mcpClient.SetStatus(McpStatus.MissingConfig); + } + } + catch (Exception e) + { + mcpClient.SetStatus(McpStatus.Error, e.Message); + } + } + + private void RegisterWithClaudeCode(string pythonDir) + { + // Resolve claude and uv; then run register command + string claudePath = ExecPath.ResolveClaude(); + if (string.IsNullOrEmpty(claudePath)) + { + UnityEngine.Debug.LogError("MCP for Unity: Claude CLI not found. Set a path in this window or install the CLI, then try again."); + return; + } + string uvPath = ExecPath.ResolveUv() ?? "uv"; + + // Prefer embedded/dev path when available + string srcDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); + if (string.IsNullOrEmpty(srcDir)) srcDir = pythonDir; + + string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{srcDir}\" server.py"; + + string projectDir = Path.GetDirectoryName(Application.dataPath); + // Ensure PATH includes common locations on Unix; on Windows leave PATH as-is + string pathPrepend = null; + if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor) + { + pathPrepend = Application.platform == RuntimePlatform.OSXEditor + ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" + : "/usr/local/bin:/usr/bin:/bin"; + } + if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) + { + string combined = ($"{stdout}\n{stderr}") ?? string.Empty; + if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0) + { + // Treat as success if Claude reports existing registration + var existingClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (existingClient != null) CheckClaudeCodeConfiguration(existingClient); + Repaint(); + UnityEngine.Debug.Log("MCP-FOR-UNITY: MCP for Unity already registered with Claude Code."); + } + else + { + UnityEngine.Debug.LogError($"MCP for Unity: Failed to start Claude CLI.\n{stderr}\n{stdout}"); + } + return; + } + + // Update status + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) CheckClaudeCodeConfiguration(claudeClient); + Repaint(); + UnityEngine.Debug.Log("MCP-FOR-UNITY: Registered with Claude Code."); + } + + private void UnregisterWithClaudeCode() + { + string claudePath = ExecPath.ResolveClaude(); + if (string.IsNullOrEmpty(claudePath)) + { + UnityEngine.Debug.LogError("MCP for Unity: Claude CLI not found. Set a path in this window or install the CLI, then try again."); + return; + } + + string projectDir = Path.GetDirectoryName(Application.dataPath); + string pathPrepend = Application.platform == RuntimePlatform.OSXEditor + ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" + : null; // On Windows, don't modify PATH - use system PATH as-is + + // Determine if Claude has a "UnityMCP" server registered by using exit codes from `claude mcp get ` + string[] candidateNamesForGet = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" }; + List existingNames = new List(); + foreach (var candidate in candidateNamesForGet) + { + if (ExecPath.TryRun(claudePath, $"mcp get {candidate}", projectDir, out var getStdout, out var getStderr, 7000, pathPrepend)) + { + // Success exit code indicates the server exists + existingNames.Add(candidate); + } + } + + if (existingNames.Count == 0) + { + // Nothing to unregister – set status and bail early + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + claudeClient.SetStatus(McpStatus.NotConfigured); + UnityEngine.Debug.Log("Claude CLI reports no MCP for Unity server via 'mcp get' - setting status to NotConfigured and aborting unregister."); + Repaint(); + } + return; + } + + // Try different possible server names + string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" }; + bool success = false; + + foreach (string serverName in possibleNames) + { + if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend)) + { + success = true; + UnityEngine.Debug.Log($"MCP for Unity: Successfully removed MCP server: {serverName}"); + break; + } + else if (!string.IsNullOrEmpty(stderr) && + !stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase)) + { + // If it's not a "not found" error, log it and stop trying + UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}"); + break; + } + } + + if (success) + { + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + // Optimistically flip to NotConfigured; then verify + claudeClient.SetStatus(McpStatus.NotConfigured); + CheckClaudeCodeConfiguration(claudeClient); + } + Repaint(); + UnityEngine.Debug.Log("MCP for Unity: MCP server successfully unregistered from Claude Code."); + } + else + { + // If no servers were found to remove, they're already unregistered + // Force status to NotConfigured and update the UI + UnityEngine.Debug.Log("No MCP servers found to unregister - already unregistered."); + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + claudeClient.SetStatus(McpStatus.NotConfigured); + CheckClaudeCodeConfiguration(claudeClient); + } + Repaint(); + } + } + + // Removed unused ParseTextOutput + + private string FindUvPath() + { + try { return MCPForUnity.Editor.Helpers.ServerInstaller.FindUvPath(); } catch { return null; } + } + + // Validation and platform-specific scanning are handled by ServerInstaller.FindUvPath() + + // Windows-specific discovery removed; use ServerInstaller.FindUvPath() instead + + // Removed unused FindClaudeCommand + + private void CheckClaudeCodeConfiguration(McpClient mcpClient) + { + try + { + // Get the Unity project directory to check project-specific config + string unityProjectDir = Application.dataPath; + string projectDir = Path.GetDirectoryName(unityProjectDir); + + // Read the global Claude config file (honor macConfigPath on macOS) + string configPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + configPath = mcpClient.windowsConfigPath; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) ? mcpClient.linuxConfigPath : mcpClient.macConfigPath; + else + configPath = mcpClient.linuxConfigPath; + + if (debugLogsEnabled) + { + MCPForUnity.Editor.Helpers.McpLog.Info($"Checking Claude config at: {configPath}", always: false); + } + + if (!File.Exists(configPath)) + { + UnityEngine.Debug.LogWarning($"Claude config file not found at: {configPath}"); + mcpClient.SetStatus(McpStatus.NotConfigured); + return; + } + + string configJson = File.ReadAllText(configPath); + dynamic claudeConfig = JsonConvert.DeserializeObject(configJson); + + // Check for "UnityMCP" server in the mcpServers section (current format) + if (claudeConfig?.mcpServers != null) + { + var servers = claudeConfig.mcpServers; + if (servers.UnityMCP != null || servers.unityMCP != null) + { + // Found MCP for Unity configured + mcpClient.SetStatus(McpStatus.Configured); + return; + } + } + + // Also check if there's a project-specific configuration for this Unity project (legacy format) + if (claudeConfig?.projects != null) + { + // Look for the project path in the config + foreach (var project in claudeConfig.projects) + { + string projectPath = project.Name; + + // Normalize paths for comparison (handle forward/back slash differences) + string normalizedProjectPath = Path.GetFullPath(projectPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + string normalizedProjectDir = Path.GetFullPath(projectDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + if (string.Equals(normalizedProjectPath, normalizedProjectDir, StringComparison.OrdinalIgnoreCase) && project.Value?.mcpServers != null) + { + // Check for "UnityMCP" (case variations) + var servers = project.Value.mcpServers; + if (servers.UnityMCP != null || servers.unityMCP != null) + { + // Found MCP for Unity configured for this project + mcpClient.SetStatus(McpStatus.Configured); + return; + } + } + } + } + + // No configuration found for this project + mcpClient.SetStatus(McpStatus.NotConfigured); + } + catch (Exception e) + { + UnityEngine.Debug.LogWarning($"Error checking Claude Code config: {e.Message}"); + mcpClient.SetStatus(McpStatus.Error, e.Message); + } + } + + private bool IsPythonDetected() + { + try + { + // Windows-specific Python detection + if (Application.platform == RuntimePlatform.WindowsEditor) + { + // Common Windows Python installation paths + string[] windowsCandidates = + { + @"C:\Python313\python.exe", + @"C:\Python312\python.exe", + @"C:\Python311\python.exe", + @"C:\Python310\python.exe", + @"C:\Python39\python.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python313\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python312\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python311\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python310\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python313\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python312\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python311\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\python.exe"), + }; + + foreach (string c in windowsCandidates) + { + if (File.Exists(c)) return true; + } + + // Try 'where python' command (Windows equivalent of 'which') + var psi = new ProcessStartInfo + { + FileName = "where", + Arguments = "python", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = Process.Start(psi); + string outp = p.StandardOutput.ReadToEnd().Trim(); + p.WaitForExit(2000); + if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp)) + { + string[] lines = outp.Split('\n'); + foreach (string line in lines) + { + string trimmed = line.Trim(); + if (File.Exists(trimmed)) return true; + } + } + } + else + { + // macOS/Linux detection (existing code) + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + string[] candidates = + { + "/opt/homebrew/bin/python3", + "/usr/local/bin/python3", + "/usr/bin/python3", + "/opt/local/bin/python3", + Path.Combine(home, ".local", "bin", "python3"), + "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3", + }; + foreach (string c in candidates) + { + if (File.Exists(c)) return true; + } + + // Try 'which python3' + var psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = "python3", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = Process.Start(psi); + string outp = p.StandardOutput.ReadToEnd().Trim(); + p.WaitForExit(2000); + if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true; + } + } + catch { } + return false; + } + } +} diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs.meta b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs.meta new file mode 100644 index 00000000..94b00cc5 --- /dev/null +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f740bec3a8d04716adeab35c412a15f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs b/MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs new file mode 100644 index 00000000..ecccbef1 --- /dev/null +++ b/MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs @@ -0,0 +1,296 @@ +using System.Runtime.InteropServices; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Windows +{ + // Editor window to display manual configuration instructions + public class ManualConfigEditorWindow : EditorWindow + { + protected string configPath; + protected string configJson; + protected Vector2 scrollPos; + protected bool pathCopied = false; + protected bool jsonCopied = false; + protected float copyFeedbackTimer = 0; + protected McpClient mcpClient; + + public static void ShowWindow(string configPath, string configJson, McpClient mcpClient) + { + var window = GetWindow("Manual Configuration"); + window.configPath = configPath; + window.configJson = configJson; + window.mcpClient = mcpClient; + window.minSize = new Vector2(500, 400); + window.Show(); + } + + protected virtual void OnGUI() + { + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + + // Header with improved styling + EditorGUILayout.Space(10); + Rect titleRect = EditorGUILayout.GetControlRect(false, 30); + EditorGUI.DrawRect( + new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), + new Color(0.2f, 0.2f, 0.2f, 0.1f) + ); + GUI.Label( + new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height), + (mcpClient?.name ?? "Unknown") + " Manual Configuration", + EditorStyles.boldLabel + ); + EditorGUILayout.Space(10); + + // Instructions with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + Rect headerRect = EditorGUILayout.GetControlRect(false, 24); + EditorGUI.DrawRect( + new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height), + new Color(0.1f, 0.1f, 0.1f, 0.2f) + ); + GUI.Label( + new Rect( + headerRect.x + 8, + headerRect.y + 4, + headerRect.width - 16, + headerRect.height + ), + "The automatic configuration failed. Please follow these steps:", + EditorStyles.boldLabel + ); + EditorGUILayout.Space(10); + + GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel) + { + margin = new RectOffset(10, 10, 5, 5), + }; + + EditorGUILayout.LabelField( + "1. Open " + (mcpClient?.name ?? "Unknown") + " config file by either:", + instructionStyle + ); + if (mcpClient?.mcpType == McpTypes.ClaudeDesktop) + { + EditorGUILayout.LabelField( + " a) Going to Settings > Developer > Edit Config", + instructionStyle + ); + } + else if (mcpClient?.mcpType == McpTypes.Cursor) + { + EditorGUILayout.LabelField( + " a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server", + instructionStyle + ); + } + else if (mcpClient?.mcpType == McpTypes.Windsurf) + { + EditorGUILayout.LabelField( + " a) Going to File > Preferences > Windsurf Settings > MCP > Manage MCPs -> View raw config", + instructionStyle + ); + } + else if (mcpClient?.mcpType == McpTypes.Kiro) + { + EditorGUILayout.LabelField( + " a) Going to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config", + instructionStyle + ); + } + else if (mcpClient?.mcpType == McpTypes.Codex) + { + EditorGUILayout.LabelField( + " a) Running `codex config edit` in a terminal", + instructionStyle + ); + } + EditorGUILayout.LabelField(" OR", instructionStyle); + EditorGUILayout.LabelField( + " b) Opening the configuration file at:", + instructionStyle + ); + + // Path section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + string displayPath; + if (mcpClient != null) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + displayPath = mcpClient.windowsConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + displayPath = string.IsNullOrEmpty(mcpClient.macConfigPath) + + ? configPath + + : mcpClient.macConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + displayPath = mcpClient.linuxConfigPath; + } + else + { + displayPath = configPath; + } + } + else + { + displayPath = configPath; + } + + // Prevent text overflow by allowing the text field to wrap + GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true }; + + EditorGUILayout.TextField( + displayPath, + pathStyle, + GUILayout.Height(EditorGUIUtility.singleLineHeight) + ); + + // Copy button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUIStyle copyButtonStyle = new(GUI.skin.button) + { + padding = new RectOffset(15, 15, 5, 5), + margin = new RectOffset(10, 10, 5, 5), + }; + + if ( + GUILayout.Button( + "Copy Path", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + EditorGUIUtility.systemCopyBuffer = displayPath; + pathCopied = true; + copyFeedbackTimer = 2f; + } + + if ( + GUILayout.Button( + "Open File", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + // Open the file using the system's default application + System.Diagnostics.Process.Start( + new System.Diagnostics.ProcessStartInfo + { + FileName = displayPath, + UseShellExecute = true, + } + ); + } + + if (pathCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + string configLabel = mcpClient?.mcpType == McpTypes.Codex + ? "2. Paste the following TOML configuration:" + : "2. Paste the following JSON configuration:"; + EditorGUILayout.LabelField(configLabel, instructionStyle); + + // JSON section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Improved text area for JSON with syntax highlighting colors + GUIStyle jsonStyle = new(EditorStyles.textArea) + { + font = EditorStyles.boldFont, + wordWrap = true, + }; + jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue + + // Draw the JSON in a text area with a taller height for better readability + EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200)); + + // Copy JSON button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if ( + GUILayout.Button( + "Copy JSON", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + EditorGUIUtility.systemCopyBuffer = configJson; + jsonCopied = true; + copyFeedbackTimer = 2f; + } + + if (jsonCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField( + "3. Save the file and restart " + (mcpClient?.name ?? "Unknown"), + instructionStyle + ); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // Close button at the bottom + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100))) + { + Close(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndScrollView(); + } + + protected virtual void Update() + { + // Handle the feedback message timer + if (copyFeedbackTimer > 0) + { + copyFeedbackTimer -= Time.deltaTime; + if (copyFeedbackTimer <= 0) + { + pathCopied = false; + jsonCopied = false; + Repaint(); + } + } + } + } +} diff --git a/MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs.meta b/MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs.meta new file mode 100644 index 00000000..41646e62 --- /dev/null +++ b/MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 36798bd7b867b8e43ac86885e94f928f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs b/MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs new file mode 100644 index 00000000..10e066d2 --- /dev/null +++ b/MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs @@ -0,0 +1,291 @@ +using System.Runtime.InteropServices; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Windows +{ + public class VSCodeManualSetupWindow : ManualConfigEditorWindow + { + public static void ShowWindow(string configPath, string configJson) + { + var window = GetWindow("VSCode GitHub Copilot Setup"); + window.configPath = configPath; + window.configJson = configJson; + window.minSize = new Vector2(550, 500); + + // Create a McpClient for VSCode + window.mcpClient = new McpClient + { + name = "VSCode GitHub Copilot", + mcpType = McpTypes.VSCode + }; + + window.Show(); + } + + protected override void OnGUI() + { + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + + // Header with improved styling + EditorGUILayout.Space(10); + Rect titleRect = EditorGUILayout.GetControlRect(false, 30); + EditorGUI.DrawRect( + new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), + new Color(0.2f, 0.2f, 0.2f, 0.1f) + ); + GUI.Label( + new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height), + "VSCode GitHub Copilot MCP Setup", + EditorStyles.boldLabel + ); + EditorGUILayout.Space(10); + + // Instructions with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + Rect headerRect = EditorGUILayout.GetControlRect(false, 24); + EditorGUI.DrawRect( + new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height), + new Color(0.1f, 0.1f, 0.1f, 0.2f) + ); + GUI.Label( + new Rect( + headerRect.x + 8, + headerRect.y + 4, + headerRect.width - 16, + headerRect.height + ), + "Setting up GitHub Copilot in VSCode with MCP for Unity", + EditorStyles.boldLabel + ); + EditorGUILayout.Space(10); + + GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel) + { + margin = new RectOffset(10, 10, 5, 5), + }; + + EditorGUILayout.LabelField( + "1. Prerequisites", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "• Ensure you have VSCode installed", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Ensure you have GitHub Copilot extension installed in VSCode", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Ensure you have a valid GitHub Copilot subscription", + instructionStyle + ); + EditorGUILayout.Space(5); + + EditorGUILayout.LabelField( + "2. Steps to Configure", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "a) Open or create your VSCode MCP config file (mcp.json) at the path below", + instructionStyle + ); + EditorGUILayout.LabelField( + "b) Paste the JSON shown below into mcp.json", + instructionStyle + ); + EditorGUILayout.LabelField( + "c) Save the file and restart VSCode", + instructionStyle + ); + EditorGUILayout.Space(5); + + EditorGUILayout.LabelField( + "3. VSCode mcp.json location:", + EditorStyles.boldLabel + ); + + // Path section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + string displayPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + displayPath = System.IO.Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), + "Code", + "User", + "mcp.json" + ); + } + else + { + displayPath = System.IO.Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Code", + "User", + "mcp.json" + ); + } + + // Store the path in the base class config path + if (string.IsNullOrEmpty(configPath)) + { + configPath = displayPath; + } + + // Prevent text overflow by allowing the text field to wrap + GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true }; + + EditorGUILayout.TextField( + displayPath, + pathStyle, + GUILayout.Height(EditorGUIUtility.singleLineHeight) + ); + + // Copy button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUIStyle copyButtonStyle = new(GUI.skin.button) + { + padding = new RectOffset(15, 15, 5, 5), + margin = new RectOffset(10, 10, 5, 5), + }; + + if ( + GUILayout.Button( + "Copy Path", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + EditorGUIUtility.systemCopyBuffer = displayPath; + pathCopied = true; + copyFeedbackTimer = 2f; + } + + if ( + GUILayout.Button( + "Open File", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + // Open the file using the system's default application + System.Diagnostics.Process.Start( + new System.Diagnostics.ProcessStartInfo + { + FileName = displayPath, + UseShellExecute = true, + } + ); + } + + if (pathCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + + EditorGUILayout.LabelField( + "4. Add this configuration to your mcp.json:", + EditorStyles.boldLabel + ); + + // JSON section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Improved text area for JSON with syntax highlighting colors + GUIStyle jsonStyle = new(EditorStyles.textArea) + { + font = EditorStyles.boldFont, + wordWrap = true, + }; + jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue + + // Draw the JSON in a text area with a taller height for better readability + EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200)); + + // Copy JSON button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if ( + GUILayout.Button( + "Copy JSON", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + EditorGUIUtility.systemCopyBuffer = configJson; + jsonCopied = true; + copyFeedbackTimer = 2f; + } + + if (jsonCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField( + "5. After configuration:", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "• Restart VSCode", + instructionStyle + ); + EditorGUILayout.LabelField( + "• GitHub Copilot will now be able to interact with your Unity project through the MCP protocol", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Remember to have the MCP for Unity Bridge running in Unity Editor", + instructionStyle + ); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // Close button at the bottom + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100))) + { + Close(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndScrollView(); + } + + protected override void Update() + { + // Call the base implementation which handles the copy feedback timer + base.Update(); + } + } +} diff --git a/MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs.meta b/MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs.meta new file mode 100644 index 00000000..fb13126b --- /dev/null +++ b/MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 377fe73d52cf0435fabead5f50a0d204 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/README.md b/MCPForUnity/README.md new file mode 100644 index 00000000..b26b9f19 --- /dev/null +++ b/MCPForUnity/README.md @@ -0,0 +1,88 @@ +# MCP for Unity — Editor Plugin Guide + +Use this guide to configure and run MCP for Unity inside the Unity Editor. Installation is covered elsewhere; this document focuses on the Editor window, client configuration, and troubleshooting. + +## Open the window +- Unity menu: Window > MCP for Unity + +The window has four areas: Server Status, Unity Bridge, MCP Client Configuration, and Script Validation. + +--- + +## Quick start +1. Open Window > MCP for Unity. +2. Click “Auto-Setup”. +3. If prompted: + - Select the server folder that contains `server.py` (UnityMcpServer~/src). + - Install Python and/or uv if missing. + - For Claude Code, ensure the `claude` CLI is installed. +4. Click “Start Bridge” if the Unity Bridge shows “Stopped”. +5. Use your MCP client (Cursor, VS Code, Windsurf, Claude Code) to connect. + +--- + +## Server Status +- Status dot and label: + - Installed / Installed (Embedded) / Not Installed. +- Mode and ports: + - Mode: Auto or Standard. + - Ports: Unity (varies; shown in UI), MCP 6500. +- Actions: + - Auto-Setup: Registers/updates your selected MCP client(s), ensures bridge connectivity. Shows “Connected ✓” after success. + - Rebuild MCP Server: Rebuilds the Python based MCP server + - Select server folder…: Choose the folder containing `server.py`. + - Verify again: Re-checks server presence. + - If Python isn’t detected, use “Open Install Instructions”. + +--- + +## Unity Bridge +- Shows Running or Stopped with a status dot. +- Start/Stop Bridge button toggles the Unity bridge process used by MCP clients to talk to Unity. +- Tip: After Auto-Setup, the bridge may auto-start in Auto mode. + +--- + +## MCP Client Configuration +- Select Client: Choose your target MCP client (e.g., Cursor, VS Code, Windsurf, Claude Code). +- Per-client actions: + - Cursor / VS Code / Windsurf: + - Auto Configure: Writes/updates your config to launch the server via uv: + - Command: uv + - Args: run --directory server.py + - Manual Setup: Opens a window with a pre-filled JSON snippet to copy/paste into your client config. + - Choose `uv` Install Location: If uv isn’t on PATH, select the uv binary. + - A compact “Config:” line shows the resolved config file name once uv/server are detected. + - Claude Code: + - Register with Claude Code / Unregister MCP for Unity with Claude Code. + - If the CLI isn’t found, click “Choose Claude Install Location”. + - The window displays the resolved Claude CLI path when detected. + +Notes: +- The UI shows a status dot and a short status text (e.g., “Configured”, “uv Not Found”, “Claude Not Found”). +- Use “Auto Configure” for one-click setup; use “Manual Setup” when you prefer to review/copy config. + +--- + +## Script Validation +- Validation Level options: + - Basic — Only syntax checks + - Standard — Syntax + Unity practices + - Comprehensive — All checks + semantic analysis + - Strict — Full semantic validation (requires Roslyn) +- Pick a level based on your project’s needs. A description is shown under the dropdown. + +--- + +## Troubleshooting +- Python or `uv` not found: + - Help: [Fix MCP for Unity with Cursor, VS Code & Windsurf](https://github.com/CoplayDev/unity-mcp/wiki/1.-Fix-Unity-MCP-and-Cursor,-VSCode-&-Windsurf) +- Claude CLI not found: + - Help: [Fix MCP for Unity with Claude Code](https://github.com/CoplayDev/unity-mcp/wiki/2.-Fix-Unity-MCP-and-Claude-Code) + +--- + +## Tips +- Enable “Show Debug Logs” in the header for more details in the Console when diagnosing issues. + +--- \ No newline at end of file diff --git a/MCPForUnity/README.md.meta b/MCPForUnity/README.md.meta new file mode 100644 index 00000000..6ef03ff5 --- /dev/null +++ b/MCPForUnity/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c3d9e362fb93e46f59ce7213fbe4f2b1 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Runtime.meta b/MCPForUnity/Runtime.meta new file mode 100644 index 00000000..ae1e4dfa --- /dev/null +++ b/MCPForUnity/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b5cc10fd969474b3680332e542416860 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef b/MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef new file mode 100644 index 00000000..52b509f8 --- /dev/null +++ b/MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "MCPForUnity.Runtime", + "rootNamespace": "MCPForUnity.Runtime", + "references": [ + "GUID:560b04d1a97f54a46a2660c3cc343a6f" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef.meta b/MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef.meta new file mode 100644 index 00000000..74c20289 --- /dev/null +++ b/MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 562a750ff18ee4193928e885c708fee1 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Runtime/Serialization.meta b/MCPForUnity/Runtime/Serialization.meta new file mode 100644 index 00000000..89cd67ad --- /dev/null +++ b/MCPForUnity/Runtime/Serialization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c7e33d6224fe6473f9bc69fe6d40e508 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs b/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs new file mode 100644 index 00000000..c76b280d --- /dev/null +++ b/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs @@ -0,0 +1,266 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using UnityEngine; +#if UNITY_EDITOR +using UnityEditor; // Required for AssetDatabase and EditorUtility +#endif + +namespace MCPForUnity.Runtime.Serialization +{ + public class Vector3Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.x); + writer.WritePropertyName("y"); + writer.WriteValue(value.y); + writer.WritePropertyName("z"); + writer.WriteValue(value.z); + writer.WriteEndObject(); + } + + public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Vector3( + (float)jo["x"], + (float)jo["y"], + (float)jo["z"] + ); + } + } + + public class Vector2Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.x); + writer.WritePropertyName("y"); + writer.WriteValue(value.y); + writer.WriteEndObject(); + } + + public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Vector2( + (float)jo["x"], + (float)jo["y"] + ); + } + } + + public class QuaternionConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.x); + writer.WritePropertyName("y"); + writer.WriteValue(value.y); + writer.WritePropertyName("z"); + writer.WriteValue(value.z); + writer.WritePropertyName("w"); + writer.WriteValue(value.w); + writer.WriteEndObject(); + } + + public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Quaternion( + (float)jo["x"], + (float)jo["y"], + (float)jo["z"], + (float)jo["w"] + ); + } + } + + public class ColorConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("r"); + writer.WriteValue(value.r); + writer.WritePropertyName("g"); + writer.WriteValue(value.g); + writer.WritePropertyName("b"); + writer.WriteValue(value.b); + writer.WritePropertyName("a"); + writer.WriteValue(value.a); + writer.WriteEndObject(); + } + + public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Color( + (float)jo["r"], + (float)jo["g"], + (float)jo["b"], + (float)jo["a"] + ); + } + } + + public class RectConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.x); + writer.WritePropertyName("y"); + writer.WriteValue(value.y); + writer.WritePropertyName("width"); + writer.WriteValue(value.width); + writer.WritePropertyName("height"); + writer.WriteValue(value.height); + writer.WriteEndObject(); + } + + public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + return new Rect( + (float)jo["x"], + (float)jo["y"], + (float)jo["width"], + (float)jo["height"] + ); + } + } + + public class BoundsConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("center"); + serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3 + writer.WritePropertyName("size"); + serializer.Serialize(writer, value.size); // Use serializer to handle nested Vector3 + writer.WriteEndObject(); + } + + public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + Vector3 center = jo["center"].ToObject(serializer); // Use serializer to handle nested Vector3 + Vector3 size = jo["size"].ToObject(serializer); // Use serializer to handle nested Vector3 + return new Bounds(center, size); + } + } + + // Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.) + public class UnityEngineObjectConverter : JsonConverter + { + public override bool CanRead => true; // We need to implement ReadJson + public override bool CanWrite => true; + + public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + +#if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only + if (UnityEditor.AssetDatabase.Contains(value)) + { + // It's an asset (Material, Texture, Prefab, etc.) + string path = UnityEditor.AssetDatabase.GetAssetPath(value); + if (!string.IsNullOrEmpty(path)) + { + writer.WriteValue(path); + } + else + { + // Asset exists but path couldn't be found? Write minimal info. + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteValue(value.name); + writer.WritePropertyName("instanceID"); + writer.WriteValue(value.GetInstanceID()); + writer.WritePropertyName("isAssetWithoutPath"); + writer.WriteValue(true); + writer.WriteEndObject(); + } + } + else + { + // It's a scene object (GameObject, Component, etc.) + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteValue(value.name); + writer.WritePropertyName("instanceID"); + writer.WriteValue(value.GetInstanceID()); + writer.WriteEndObject(); + } +#else + // Runtime fallback: Write basic info without AssetDatabase + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteValue(value.name); + writer.WritePropertyName("instanceID"); + writer.WriteValue(value.GetInstanceID()); + writer.WritePropertyName("warning"); + writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable."); + writer.WriteEndObject(); +#endif + } + + public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + +#if UNITY_EDITOR + if (reader.TokenType == JsonToken.String) + { + // Assume it's an asset path + string path = reader.Value.ToString(); + return UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType); + } + + if (reader.TokenType == JsonToken.StartObject) + { + JObject jo = JObject.Load(reader); + if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer) + { + int instanceId = idToken.ToObject(); + UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId); + if (obj != null && objectType.IsAssignableFrom(obj.GetType())) + { + return obj; + } + } + // Could potentially try finding by name as a fallback if ID lookup fails/isn't present + // but that's less reliable. + } +#else + // Runtime deserialization is tricky without AssetDatabase/EditorUtility + // Maybe log a warning and return null or existingValue? + Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode."); + // Skip the token to avoid breaking the reader + if (reader.TokenType == JsonToken.StartObject) JObject.Load(reader); + else if (reader.TokenType == JsonToken.String) reader.ReadAsString(); + // Return null or existing value, depending on desired behavior + return existingValue; +#endif + + throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object"); + } + } +} \ No newline at end of file diff --git a/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs.meta b/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs.meta new file mode 100644 index 00000000..caaf2859 --- /dev/null +++ b/MCPForUnity/Runtime/Serialization/UnityTypeConverters.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e65311c160f0d41d4a1b45a3dba8dd5a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/UnityMcpServer~/src/Dockerfile b/MCPForUnity/UnityMcpServer~/src/Dockerfile new file mode 100644 index 00000000..5fcbc4eb --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.12-slim + +# Install required system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Install uv package manager +RUN pip install uv + +# Copy required files +COPY config.py /app/ +COPY server.py /app/ +COPY unity_connection.py /app/ +COPY pyproject.toml /app/ +COPY __init__.py /app/ +COPY tools/ /app/tools/ + +# Install dependencies using uv +RUN uv pip install --system -e . + + +# Command to run the server +CMD ["uv", "run", "server.py"] diff --git a/MCPForUnity/UnityMcpServer~/src/__init__.py b/MCPForUnity/UnityMcpServer~/src/__init__.py new file mode 100644 index 00000000..ad59ec7c --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/__init__.py @@ -0,0 +1,3 @@ +""" +MCP for Unity Server package. +""" diff --git a/MCPForUnity/UnityMcpServer~/src/config.py b/MCPForUnity/UnityMcpServer~/src/config.py new file mode 100644 index 00000000..526522da --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/config.py @@ -0,0 +1,48 @@ +""" +Configuration settings for the MCP for Unity Server. +This file contains all configurable parameters for the server. +""" + +from dataclasses import dataclass + + +@dataclass +class ServerConfig: + """Main configuration class for the MCP server.""" + + # Network settings + unity_host: str = "localhost" + unity_port: int = 6400 + mcp_port: int = 6500 + + # Connection settings + # short initial timeout; retries use shorter timeouts + connection_timeout: float = 1.0 + buffer_size: int = 16 * 1024 * 1024 # 16MB buffer + # Framed receive behavior + # max seconds to wait while consuming heartbeats only + framed_receive_timeout: float = 2.0 + # cap heartbeat frames consumed before giving up + max_heartbeat_frames: int = 16 + + # Logging settings + log_level: str = "INFO" + log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + # Server settings + max_retries: int = 10 + retry_delay: float = 0.25 + # Backoff hint returned to clients when Unity is reloading (milliseconds) + reload_retry_ms: int = 250 + # Number of polite retries when Unity reports reloading + # 40 × 250ms ≈ 10s default window + reload_max_retries: int = 40 + + # Telemetry settings + telemetry_enabled: bool = True + # Align with telemetry.py default Cloud Run endpoint + telemetry_endpoint: str = "https://api-prod.coplay.dev/telemetry/events" + + +# Create a global config instance +config = ServerConfig() diff --git a/MCPForUnity/UnityMcpServer~/src/port_discovery.py b/MCPForUnity/UnityMcpServer~/src/port_discovery.py new file mode 100644 index 00000000..b936f967 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/port_discovery.py @@ -0,0 +1,160 @@ +""" +Port discovery utility for MCP for Unity Server. + +What changed and why: +- Unity now writes a per-project port file named like + `~/.unity-mcp/unity-mcp-port-.json` to avoid projects overwriting + each other's saved port. The legacy file `unity-mcp-port.json` may still + exist. +- This module now scans for both patterns, prefers the most recently + modified file, and verifies that the port is actually a MCP for Unity listener + (quick socket connect + ping) before choosing it. +""" + +import glob +import json +import logging +from pathlib import Path +import socket +from typing import Optional, List + +logger = logging.getLogger("mcp-for-unity-server") + + +class PortDiscovery: + """Handles port discovery from Unity Bridge registry""" + REGISTRY_FILE = "unity-mcp-port.json" # legacy single-project file + DEFAULT_PORT = 6400 + CONNECT_TIMEOUT = 0.3 # seconds, keep this snappy during discovery + + @staticmethod + def get_registry_path() -> Path: + """Get the path to the port registry file""" + return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE + + @staticmethod + def get_registry_dir() -> Path: + return Path.home() / ".unity-mcp" + + @staticmethod + def list_candidate_files() -> List[Path]: + """Return candidate registry files, newest first. + Includes hashed per-project files and the legacy file (if present). + """ + base = PortDiscovery.get_registry_dir() + hashed = sorted( + (Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))), + key=lambda p: p.stat().st_mtime, + reverse=True, + ) + legacy = PortDiscovery.get_registry_path() + if legacy.exists(): + # Put legacy at the end so hashed, per-project files win + hashed.append(legacy) + return hashed + + @staticmethod + def _try_probe_unity_mcp(port: int) -> bool: + """Quickly check if a MCP for Unity listener is on this port. + Tries a short TCP connect, sends 'ping', expects a JSON 'pong'. + """ + try: + with socket.create_connection(("127.0.0.1", port), PortDiscovery.CONNECT_TIMEOUT) as s: + s.settimeout(PortDiscovery.CONNECT_TIMEOUT) + try: + s.sendall(b"ping") + data = s.recv(512) + # Minimal validation: look for a success pong response + if data and b'"message":"pong"' in data: + return True + except Exception: + return False + except Exception: + return False + return False + + @staticmethod + def _read_latest_status() -> Optional[dict]: + try: + base = PortDiscovery.get_registry_dir() + status_files = sorted( + (Path(p) + for p in glob.glob(str(base / "unity-mcp-status-*.json"))), + key=lambda p: p.stat().st_mtime, + reverse=True, + ) + if not status_files: + return None + with status_files[0].open('r') as f: + return json.load(f) + except Exception: + return None + + @staticmethod + def discover_unity_port() -> int: + """ + Discover Unity port by scanning per-project and legacy registry files. + Prefer the newest file whose port responds; fall back to first parsed + value; finally default to 6400. + + Returns: + Port number to connect to + """ + # Prefer the latest heartbeat status if it points to a responsive port + status = PortDiscovery._read_latest_status() + if status: + port = status.get('unity_port') + if isinstance(port, int) and PortDiscovery._try_probe_unity_mcp(port): + logger.info(f"Using Unity port from status: {port}") + return port + + candidates = PortDiscovery.list_candidate_files() + + first_seen_port: Optional[int] = None + + for path in candidates: + try: + with open(path, 'r') as f: + cfg = json.load(f) + unity_port = cfg.get('unity_port') + if isinstance(unity_port, int): + if first_seen_port is None: + first_seen_port = unity_port + if PortDiscovery._try_probe_unity_mcp(unity_port): + logger.info( + f"Using Unity port from {path.name}: {unity_port}") + return unity_port + except Exception as e: + logger.warning(f"Could not read port registry {path}: {e}") + + if first_seen_port is not None: + logger.info( + f"No responsive port found; using first seen value {first_seen_port}") + return first_seen_port + + # Fallback to default port + logger.info( + f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}") + return PortDiscovery.DEFAULT_PORT + + @staticmethod + def get_port_config() -> Optional[dict]: + """ + Get the most relevant port configuration from registry. + Returns the most recent hashed file's config if present, + otherwise the legacy file's config. Returns None if nothing exists. + + Returns: + Port configuration dict or None if not found + """ + candidates = PortDiscovery.list_candidate_files() + if not candidates: + return None + for path in candidates: + try: + with open(path, 'r') as f: + return json.load(f) + except Exception as e: + logger.warning( + f"Could not read port configuration {path}: {e}") + return None diff --git a/MCPForUnity/UnityMcpServer~/src/pyproject.toml b/MCPForUnity/UnityMcpServer~/src/pyproject.toml new file mode 100644 index 00000000..d6d6d00a --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "MCPForUnityServer" +version = "4.1.1" +description = "MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)." +readme = "README.md" +requires-python = ">=3.10" +dependencies = ["httpx>=0.27.2", "mcp[cli]>=1.15.0"] + +[build-system] +requires = ["setuptools>=64.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +py-modules = ["config", "server", "unity_connection"] +packages = ["tools"] diff --git a/MCPForUnity/UnityMcpServer~/src/pyrightconfig.json b/MCPForUnity/UnityMcpServer~/src/pyrightconfig.json new file mode 100644 index 00000000..4fdeb465 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/pyrightconfig.json @@ -0,0 +1,11 @@ +{ + "typeCheckingMode": "basic", + "reportMissingImports": "none", + "pythonVersion": "3.11", + "executionEnvironments": [ + { + "root": ".", + "pythonVersion": "3.11" + } + ] +} diff --git a/MCPForUnity/UnityMcpServer~/src/registry/__init__.py b/MCPForUnity/UnityMcpServer~/src/registry/__init__.py new file mode 100644 index 00000000..5beb708b --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/registry/__init__.py @@ -0,0 +1,14 @@ +""" +Registry package for MCP tool auto-discovery. +""" +from .tool_registry import ( + mcp_for_unity_tool, + get_registered_tools, + clear_registry +) + +__all__ = [ + 'mcp_for_unity_tool', + 'get_registered_tools', + 'clear_registry' +] diff --git a/MCPForUnity/UnityMcpServer~/src/registry/tool_registry.py b/MCPForUnity/UnityMcpServer~/src/registry/tool_registry.py new file mode 100644 index 00000000..bbe36439 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/registry/tool_registry.py @@ -0,0 +1,51 @@ +""" +Tool registry for auto-discovery of MCP tools. +""" +from typing import Callable, Any + +# Global registry to collect decorated tools +_tool_registry: list[dict[str, Any]] = [] + + +def mcp_for_unity_tool( + name: str | None = None, + description: str | None = None, + **kwargs +) -> Callable: + """ + Decorator for registering MCP tools in the server's tools directory. + + Tools are registered in the global tool registry. + + Args: + name: Tool name (defaults to function name) + description: Tool description + **kwargs: Additional arguments passed to @mcp.tool() + + Example: + @mcp_for_unity_tool(description="Does something cool") + async def my_custom_tool(ctx: Context, ...): + pass + """ + def decorator(func: Callable) -> Callable: + tool_name = name if name is not None else func.__name__ + _tool_registry.append({ + 'func': func, + 'name': tool_name, + 'description': description, + 'kwargs': kwargs + }) + + return func + + return decorator + + +def get_registered_tools() -> list[dict[str, Any]]: + """Get all registered tools""" + return _tool_registry.copy() + + +def clear_registry(): + """Clear the tool registry (useful for testing)""" + _tool_registry.clear() diff --git a/MCPForUnity/UnityMcpServer~/src/reload_sentinel.py b/MCPForUnity/UnityMcpServer~/src/reload_sentinel.py new file mode 100644 index 00000000..71e5f623 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/reload_sentinel.py @@ -0,0 +1,9 @@ +""" +Deprecated: Sentinel flipping is handled inside Unity via the MCP menu +'MCP/Flip Reload Sentinel'. This module remains only as a compatibility shim. +All functions are no-ops to prevent accidental external writes. +""" + + +def flip_reload_sentinel(*args, **kwargs) -> str: + return "reload_sentinel.py is deprecated; use execute_menu_item → 'MCP/Flip Reload Sentinel'" diff --git a/MCPForUnity/UnityMcpServer~/src/server.py b/MCPForUnity/UnityMcpServer~/src/server.py new file mode 100644 index 00000000..af6fe036 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/server.py @@ -0,0 +1,194 @@ +from telemetry import record_telemetry, record_milestone, RecordType, MilestoneType +from mcp.server.fastmcp import FastMCP +import logging +from logging.handlers import RotatingFileHandler +import os +from contextlib import asynccontextmanager +from typing import AsyncIterator, Dict, Any +from config import config +from tools import register_all_tools +from unity_connection import get_unity_connection, UnityConnection +import time + +# Configure logging using settings from config +logging.basicConfig( + level=getattr(logging, config.log_level), + format=config.log_format, + stream=None, # None -> defaults to sys.stderr; avoid stdout used by MCP stdio + force=True # Ensure our handler replaces any prior stdout handlers +) +logger = logging.getLogger("mcp-for-unity-server") + +# Also write logs to a rotating file so logs are available when launched via stdio +try: + import os as _os + _log_dir = _os.path.join(_os.path.expanduser( + "~/Library/Application Support/UnityMCP"), "Logs") + _os.makedirs(_log_dir, exist_ok=True) + _file_path = _os.path.join(_log_dir, "unity_mcp_server.log") + _fh = RotatingFileHandler( + _file_path, maxBytes=512*1024, backupCount=2, encoding="utf-8") + _fh.setFormatter(logging.Formatter(config.log_format)) + _fh.setLevel(getattr(logging, config.log_level)) + logger.addHandler(_fh) + # Also route telemetry logger to the same rotating file and normal level + try: + tlog = logging.getLogger("unity-mcp-telemetry") + tlog.setLevel(getattr(logging, config.log_level)) + tlog.addHandler(_fh) + except Exception: + # Never let logging setup break startup + pass +except Exception: + # Never let logging setup break startup + pass +# Quieten noisy third-party loggers to avoid clutter during stdio handshake +for noisy in ("httpx", "urllib3"): + try: + logging.getLogger(noisy).setLevel( + max(logging.WARNING, getattr(logging, config.log_level))) + except Exception: + pass + +# Import telemetry only after logging is configured to ensure its logs use stderr and proper levels +# Ensure a slightly higher telemetry timeout unless explicitly overridden by env +try: + + # Ensure generous timeout unless explicitly overridden by env + if not os.environ.get("UNITY_MCP_TELEMETRY_TIMEOUT"): + os.environ["UNITY_MCP_TELEMETRY_TIMEOUT"] = "5.0" +except Exception: + pass + +# Global connection state +_unity_connection: UnityConnection = None + + +@asynccontextmanager +async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: + """Handle server startup and shutdown.""" + global _unity_connection + logger.info("MCP for Unity Server starting up") + + # Record server startup telemetry + start_time = time.time() + start_clk = time.perf_counter() + try: + from pathlib import Path + ver_path = Path(__file__).parent / "server_version.txt" + server_version = ver_path.read_text(encoding="utf-8").strip() + except Exception: + server_version = "unknown" + # Defer initial telemetry by 1s to avoid stdio handshake interference + import threading + + def _emit_startup(): + try: + record_telemetry(RecordType.STARTUP, { + "server_version": server_version, + "startup_time": start_time, + }) + record_milestone(MilestoneType.FIRST_STARTUP) + except Exception: + logger.debug("Deferred startup telemetry failed", exc_info=True) + threading.Timer(1.0, _emit_startup).start() + + try: + skip_connect = os.environ.get( + "UNITY_MCP_SKIP_STARTUP_CONNECT", "").lower() in ("1", "true", "yes", "on") + if skip_connect: + logger.info( + "Skipping Unity connection on startup (UNITY_MCP_SKIP_STARTUP_CONNECT=1)") + else: + _unity_connection = get_unity_connection() + logger.info("Connected to Unity on startup") + + # Record successful Unity connection (deferred) + import threading as _t + _t.Timer(1.0, lambda: record_telemetry( + RecordType.UNITY_CONNECTION, + { + "status": "connected", + "connection_time_ms": (time.perf_counter() - start_clk) * 1000, + } + )).start() + + except ConnectionError as e: + logger.warning("Could not connect to Unity on startup: %s", e) + _unity_connection = None + + # Record connection failure (deferred) + import threading as _t + _err_msg = str(e)[:200] + _t.Timer(1.0, lambda: record_telemetry( + RecordType.UNITY_CONNECTION, + { + "status": "failed", + "error": _err_msg, + "connection_time_ms": (time.perf_counter() - start_clk) * 1000, + } + )).start() + except Exception as e: + logger.warning( + "Unexpected error connecting to Unity on startup: %s", e) + _unity_connection = None + import threading as _t + _err_msg = str(e)[:200] + _t.Timer(1.0, lambda: record_telemetry( + RecordType.UNITY_CONNECTION, + { + "status": "failed", + "error": _err_msg, + "connection_time_ms": (time.perf_counter() - start_clk) * 1000, + } + )).start() + + try: + # Yield the connection object so it can be attached to the context + # The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge) + yield {"bridge": _unity_connection} + finally: + if _unity_connection: + _unity_connection.disconnect() + _unity_connection = None + logger.info("MCP for Unity Server shut down") + +# Initialize MCP server +mcp = FastMCP( + name="mcp-for-unity-server", + lifespan=server_lifespan +) + +# Register all tools +register_all_tools(mcp) + +# Asset Creation Strategy + + +@mcp.prompt() +def asset_creation_strategy() -> str: + """Guide for discovering and using MCP for Unity tools effectively.""" + return ( + "Available MCP for Unity Server Tools:\n\n" + "- `manage_editor`: Controls editor state and queries info.\n" + "- `manage_menu_item`: Executes, lists and checks for the existence of Unity Editor menu items.\n" + "- `read_console`: Reads or clears Unity console messages, with filtering options.\n" + "- `manage_scene`: Manages scenes.\n" + "- `manage_gameobject`: Manages GameObjects in the scene.\n" + "- `manage_script`: Manages C# script files.\n" + "- `manage_asset`: Manages prefabs and assets.\n" + "- `manage_shader`: Manages shaders.\n\n" + "Tips:\n" + "- Create prefabs for reusable GameObjects.\n" + "- Always include a camera and main light in your scenes.\n" + "- Unless specified otherwise, paths are relative to the project's `Assets/` folder.\n" + "- After creating or modifying scripts with `manage_script`, allow Unity to recompile; use `read_console` to check for compile errors.\n" + "- Use `manage_menu_item` for interacting with Unity systems and third party tools like a user would.\n" + "- List menu items before using them if you are unsure of the menu path.\n" + "- If a menu item seems missing, refresh the cache: use manage_menu_item with action='list' and refresh=true, or action='refresh'. Avoid refreshing every time; prefer refresh only when the menu set likely changed.\n" + ) + + +# Run the server +if __name__ == "__main__": + mcp.run(transport='stdio') diff --git a/MCPForUnity/UnityMcpServer~/src/server_version.txt b/MCPForUnity/UnityMcpServer~/src/server_version.txt new file mode 100644 index 00000000..627a3f43 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/server_version.txt @@ -0,0 +1 @@ +4.1.1 diff --git a/MCPForUnity/UnityMcpServer~/src/telemetry.py b/MCPForUnity/UnityMcpServer~/src/telemetry.py new file mode 100644 index 00000000..bfcfe76a --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/telemetry.py @@ -0,0 +1,460 @@ +""" +Privacy-focused, anonymous telemetry system for Unity MCP +Inspired by Onyx's telemetry implementation with Unity-specific adaptations + +Fire-and-forget telemetry sender with a single background worker. +- No context/thread-local propagation to avoid re-entrancy into tool resolution. +- Small network timeouts to prevent stalls. +""" + +import contextlib +from dataclasses import dataclass +from enum import Enum +import importlib +import json +import logging +import os +from pathlib import Path +import platform +import queue +import sys +import threading +import time +from typing import Optional, Dict, Any +from urllib.parse import urlparse +import uuid + +try: + import httpx + HAS_HTTPX = True +except ImportError: + httpx = None # type: ignore + HAS_HTTPX = False + +logger = logging.getLogger("unity-mcp-telemetry") + + +class RecordType(str, Enum): + """Types of telemetry records we collect""" + VERSION = "version" + STARTUP = "startup" + USAGE = "usage" + LATENCY = "latency" + FAILURE = "failure" + TOOL_EXECUTION = "tool_execution" + UNITY_CONNECTION = "unity_connection" + CLIENT_CONNECTION = "client_connection" + + +class MilestoneType(str, Enum): + """Major user journey milestones""" + FIRST_STARTUP = "first_startup" + FIRST_TOOL_USAGE = "first_tool_usage" + FIRST_SCRIPT_CREATION = "first_script_creation" + FIRST_SCENE_MODIFICATION = "first_scene_modification" + MULTIPLE_SESSIONS = "multiple_sessions" + DAILY_ACTIVE_USER = "daily_active_user" + WEEKLY_ACTIVE_USER = "weekly_active_user" + + +@dataclass +class TelemetryRecord: + """Structure for telemetry data""" + record_type: RecordType + timestamp: float + customer_uuid: str + session_id: str + data: Dict[str, Any] + milestone: Optional[MilestoneType] = None + + +class TelemetryConfig: + """Telemetry configuration""" + + def __init__(self): + # Prefer config file, then allow env overrides + server_config = None + for modname in ( + "MCPForUnity.UnityMcpServer~.src.config", + "MCPForUnity.UnityMcpServer.src.config", + "src.config", + "config", + ): + try: + mod = importlib.import_module(modname) + server_config = getattr(mod, "config", None) + if server_config is not None: + break + except Exception: + continue + + # Determine enabled flag: config -> env DISABLE_* opt-out + cfg_enabled = True if server_config is None else bool( + getattr(server_config, "telemetry_enabled", True)) + self.enabled = cfg_enabled and not self._is_disabled() + + # Telemetry endpoint (Cloud Run default; override via env) + cfg_default = None if server_config is None else getattr( + server_config, "telemetry_endpoint", None) + default_ep = cfg_default or "https://api-prod.coplay.dev/telemetry/events" + self.default_endpoint = default_ep + self.endpoint = self._validated_endpoint( + os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT", default_ep), + default_ep, + ) + try: + logger.info( + "Telemetry configured: endpoint=%s (default=%s), timeout_env=%s", + self.endpoint, + default_ep, + os.environ.get("UNITY_MCP_TELEMETRY_TIMEOUT") or "" + ) + except Exception: + pass + + # Local storage for UUID and milestones + self.data_dir = self._get_data_directory() + self.uuid_file = self.data_dir / "customer_uuid.txt" + self.milestones_file = self.data_dir / "milestones.json" + + # Request timeout (small, fail fast). Override with UNITY_MCP_TELEMETRY_TIMEOUT + try: + self.timeout = float(os.environ.get( + "UNITY_MCP_TELEMETRY_TIMEOUT", "1.5")) + except Exception: + self.timeout = 1.5 + try: + logger.info("Telemetry timeout=%.2fs", self.timeout) + except Exception: + pass + + # Session tracking + self.session_id = str(uuid.uuid4()) + + def _is_disabled(self) -> bool: + """Check if telemetry is disabled via environment variables""" + disable_vars = [ + "DISABLE_TELEMETRY", + "UNITY_MCP_DISABLE_TELEMETRY", + "MCP_DISABLE_TELEMETRY" + ] + + for var in disable_vars: + if os.environ.get(var, "").lower() in ("true", "1", "yes", "on"): + return True + return False + + def _get_data_directory(self) -> Path: + """Get directory for storing telemetry data""" + if os.name == 'nt': # Windows + base_dir = Path(os.environ.get( + 'APPDATA', Path.home() / 'AppData' / 'Roaming')) + elif os.name == 'posix': # macOS/Linux + if 'darwin' in os.uname().sysname.lower(): # macOS + base_dir = Path.home() / 'Library' / 'Application Support' + else: # Linux + base_dir = Path(os.environ.get('XDG_DATA_HOME', + Path.home() / '.local' / 'share')) + else: + base_dir = Path.home() / '.unity-mcp' + + data_dir = base_dir / 'UnityMCP' + data_dir.mkdir(parents=True, exist_ok=True) + return data_dir + + def _validated_endpoint(self, candidate: str, fallback: str) -> str: + """Validate telemetry endpoint URL scheme; allow only http/https. + Falls back to the provided default on error. + """ + try: + parsed = urlparse(candidate) + if parsed.scheme not in ("https", "http"): + raise ValueError(f"Unsupported scheme: {parsed.scheme}") + # Basic sanity: require network location and path + if not parsed.netloc: + raise ValueError("Missing netloc in endpoint") + # Reject localhost/loopback endpoints in production to avoid accidental local overrides + host = parsed.hostname or "" + if host in ("localhost", "127.0.0.1", "::1"): + raise ValueError( + "Localhost endpoints are not allowed for telemetry") + return candidate + except Exception as e: + logger.debug( + f"Invalid telemetry endpoint '{candidate}', using default. Error: {e}", + exc_info=True, + ) + return fallback + + +class TelemetryCollector: + """Main telemetry collection class""" + + def __init__(self): + self.config = TelemetryConfig() + self._customer_uuid: Optional[str] = None + self._milestones: Dict[str, Dict[str, Any]] = {} + self._lock: threading.Lock = threading.Lock() + # Bounded queue with single background worker (records only; no context propagation) + self._queue: "queue.Queue[TelemetryRecord]" = queue.Queue(maxsize=1000) + # Load persistent data before starting worker so first events have UUID + self._load_persistent_data() + self._worker: threading.Thread = threading.Thread( + target=self._worker_loop, daemon=True) + self._worker.start() + + def _load_persistent_data(self): + """Load UUID and milestones from disk""" + # Load customer UUID + try: + if self.config.uuid_file.exists(): + self._customer_uuid = self.config.uuid_file.read_text( + encoding="utf-8").strip() or str(uuid.uuid4()) + else: + self._customer_uuid = str(uuid.uuid4()) + try: + self.config.uuid_file.write_text( + self._customer_uuid, encoding="utf-8") + if os.name == "posix": + os.chmod(self.config.uuid_file, 0o600) + except OSError as e: + logger.debug( + f"Failed to persist customer UUID: {e}", exc_info=True) + except OSError as e: + logger.debug(f"Failed to load customer UUID: {e}", exc_info=True) + self._customer_uuid = str(uuid.uuid4()) + + # Load milestones (failure here must not affect UUID) + try: + if self.config.milestones_file.exists(): + content = self.config.milestones_file.read_text( + encoding="utf-8") + self._milestones = json.loads(content) or {} + if not isinstance(self._milestones, dict): + self._milestones = {} + except (OSError, json.JSONDecodeError, ValueError) as e: + logger.debug(f"Failed to load milestones: {e}", exc_info=True) + self._milestones = {} + + def _save_milestones(self): + """Save milestones to disk. Caller must hold self._lock.""" + try: + self.config.milestones_file.write_text( + json.dumps(self._milestones, indent=2), + encoding="utf-8", + ) + except OSError as e: + logger.warning(f"Failed to save milestones: {e}", exc_info=True) + + def record_milestone(self, milestone: MilestoneType, data: Optional[Dict[str, Any]] = None) -> bool: + """Record a milestone event, returns True if this is the first occurrence""" + if not self.config.enabled: + return False + milestone_key = milestone.value + with self._lock: + if milestone_key in self._milestones: + return False # Already recorded + milestone_data = { + "timestamp": time.time(), + "data": data or {}, + } + self._milestones[milestone_key] = milestone_data + self._save_milestones() + + # Also send as telemetry record + self.record( + record_type=RecordType.USAGE, + data={"milestone": milestone_key, **(data or {})}, + milestone=milestone + ) + + return True + + def record(self, + record_type: RecordType, + data: Dict[str, Any], + milestone: Optional[MilestoneType] = None): + """Record a telemetry event (async, non-blocking)""" + if not self.config.enabled: + return + + # Allow fallback sender when httpx is unavailable (no early return) + + record = TelemetryRecord( + record_type=record_type, + timestamp=time.time(), + customer_uuid=self._customer_uuid or "unknown", + session_id=self.config.session_id, + data=data, + milestone=milestone + ) + # Enqueue for background worker (non-blocking). Drop on backpressure. + try: + self._queue.put_nowait(record) + except queue.Full: + logger.debug("Telemetry queue full; dropping %s", + record.record_type) + + def _worker_loop(self): + """Background worker that serializes telemetry sends.""" + while True: + rec = self._queue.get() + try: + # Run sender directly; do not reuse caller context/thread-locals + self._send_telemetry(rec) + except Exception: + logger.debug("Telemetry worker send failed", exc_info=True) + finally: + with contextlib.suppress(Exception): + self._queue.task_done() + + def _send_telemetry(self, record: TelemetryRecord): + """Send telemetry data to endpoint""" + try: + # System fingerprint (top-level remains concise; details stored in data JSON) + _platform = platform.system() # 'Darwin' | 'Linux' | 'Windows' + _source = sys.platform # 'darwin' | 'linux' | 'win32' + _platform_detail = f"{_platform} {platform.release()} ({platform.machine()})" + _python_version = platform.python_version() + + # Enrich data JSON so BigQuery stores detailed fields without schema change + enriched_data = dict(record.data or {}) + enriched_data.setdefault("platform_detail", _platform_detail) + enriched_data.setdefault("python_version", _python_version) + + payload = { + "record": record.record_type.value, + "timestamp": record.timestamp, + "customer_uuid": record.customer_uuid, + "session_id": record.session_id, + "data": enriched_data, + "version": "3.0.2", # Unity MCP version + "platform": _platform, + "source": _source, + } + + if record.milestone: + payload["milestone"] = record.milestone.value + + # Prefer httpx when available; otherwise fall back to urllib + if httpx: + with httpx.Client(timeout=self.config.timeout) as client: + # Re-validate endpoint at send time to handle dynamic changes + endpoint = self.config._validated_endpoint( + self.config.endpoint, self.config.default_endpoint) + response = client.post(endpoint, json=payload) + if 200 <= response.status_code < 300: + logger.debug(f"Telemetry sent: {record.record_type}") + else: + logger.warning( + f"Telemetry failed: HTTP {response.status_code}") + else: + import urllib.request + import urllib.error + data_bytes = json.dumps(payload).encode("utf-8") + endpoint = self.config._validated_endpoint( + self.config.endpoint, self.config.default_endpoint) + req = urllib.request.Request( + endpoint, + data=data_bytes, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=self.config.timeout) as resp: + if 200 <= resp.getcode() < 300: + logger.debug( + f"Telemetry sent (urllib): {record.record_type}") + else: + logger.warning( + f"Telemetry failed (urllib): HTTP {resp.getcode()}") + except urllib.error.URLError as ue: + logger.warning(f"Telemetry send failed (urllib): {ue}") + + except Exception as e: + # Never let telemetry errors interfere with app functionality + logger.debug(f"Telemetry send failed: {e}") + + +# Global telemetry instance +_telemetry_collector: Optional[TelemetryCollector] = None + + +def get_telemetry() -> TelemetryCollector: + """Get the global telemetry collector instance""" + global _telemetry_collector + if _telemetry_collector is None: + _telemetry_collector = TelemetryCollector() + return _telemetry_collector + + +def record_telemetry(record_type: RecordType, + data: Dict[str, Any], + milestone: Optional[MilestoneType] = None): + """Convenience function to record telemetry""" + get_telemetry().record(record_type, data, milestone) + + +def record_milestone(milestone: MilestoneType, data: Optional[Dict[str, Any]] = None) -> bool: + """Convenience function to record a milestone""" + return get_telemetry().record_milestone(milestone, data) + + +def record_tool_usage(tool_name: str, success: bool, duration_ms: float, error: Optional[str] = None, sub_action: Optional[str] = None): + """Record tool usage telemetry + + Args: + tool_name: Name of the tool invoked (e.g., 'manage_scene'). + success: Whether the tool completed successfully. + duration_ms: Execution duration in milliseconds. + error: Optional error message (truncated if present). + sub_action: Optional sub-action/operation within the tool (e.g., 'get_hierarchy'). + """ + data = { + "tool_name": tool_name, + "success": success, + "duration_ms": round(duration_ms, 2) + } + + if sub_action is not None: + try: + data["sub_action"] = str(sub_action) + except Exception: + # Ensure telemetry is never disruptive + data["sub_action"] = "unknown" + + if error: + data["error"] = str(error)[:200] # Limit error message length + + record_telemetry(RecordType.TOOL_EXECUTION, data) + + +def record_latency(operation: str, duration_ms: float, metadata: Optional[Dict[str, Any]] = None): + """Record latency telemetry""" + data = { + "operation": operation, + "duration_ms": round(duration_ms, 2) + } + + if metadata: + data.update(metadata) + + record_telemetry(RecordType.LATENCY, data) + + +def record_failure(component: str, error: str, metadata: Optional[Dict[str, Any]] = None): + """Record failure telemetry""" + data = { + "component": component, + "error": str(error)[:500] # Limit error message length + } + + if metadata: + data.update(metadata) + + record_telemetry(RecordType.FAILURE, data) + + +def is_telemetry_enabled() -> bool: + """Check if telemetry is enabled""" + return get_telemetry().config.enabled diff --git a/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py b/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py new file mode 100644 index 00000000..7e892809 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py @@ -0,0 +1,107 @@ +""" +Telemetry decorator for Unity MCP tools +""" + +import functools +import inspect +import logging +import time +from typing import Callable, Any + +from telemetry import record_tool_usage, record_milestone, MilestoneType + +_log = logging.getLogger("unity-mcp-telemetry") +_decorator_log_count = 0 + + +def telemetry_tool(tool_name: str): + """Decorator to add telemetry tracking to MCP tools""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def _sync_wrapper(*args, **kwargs) -> Any: + start_time = time.time() + success = False + error = None + # Extract sub-action (e.g., 'get_hierarchy') from bound args when available + sub_action = None + try: + sig = inspect.signature(func) + bound = sig.bind_partial(*args, **kwargs) + bound.apply_defaults() + sub_action = bound.arguments.get("action") + except Exception: + sub_action = None + try: + global _decorator_log_count + if _decorator_log_count < 10: + _log.info(f"telemetry_decorator sync: tool={tool_name}") + _decorator_log_count += 1 + result = func(*args, **kwargs) + success = True + action_val = sub_action or kwargs.get("action") + try: + if tool_name == "manage_script" and action_val == "create": + record_milestone(MilestoneType.FIRST_SCRIPT_CREATION) + elif tool_name.startswith("manage_scene"): + record_milestone( + MilestoneType.FIRST_SCENE_MODIFICATION) + record_milestone(MilestoneType.FIRST_TOOL_USAGE) + except Exception: + _log.debug("milestone emit failed", exc_info=True) + return result + except Exception as e: + error = str(e) + raise + finally: + duration_ms = (time.time() - start_time) * 1000 + try: + record_tool_usage(tool_name, success, + duration_ms, error, sub_action=sub_action) + except Exception: + _log.debug("record_tool_usage failed", exc_info=True) + + @functools.wraps(func) + async def _async_wrapper(*args, **kwargs) -> Any: + start_time = time.time() + success = False + error = None + # Extract sub-action (e.g., 'get_hierarchy') from bound args when available + sub_action = None + try: + sig = inspect.signature(func) + bound = sig.bind_partial(*args, **kwargs) + bound.apply_defaults() + sub_action = bound.arguments.get("action") + except Exception: + sub_action = None + try: + global _decorator_log_count + if _decorator_log_count < 10: + _log.info(f"telemetry_decorator async: tool={tool_name}") + _decorator_log_count += 1 + result = await func(*args, **kwargs) + success = True + action_val = sub_action or kwargs.get("action") + try: + if tool_name == "manage_script" and action_val == "create": + record_milestone(MilestoneType.FIRST_SCRIPT_CREATION) + elif tool_name.startswith("manage_scene"): + record_milestone( + MilestoneType.FIRST_SCENE_MODIFICATION) + record_milestone(MilestoneType.FIRST_TOOL_USAGE) + except Exception: + _log.debug("milestone emit failed", exc_info=True) + return result + except Exception as e: + error = str(e) + raise + finally: + duration_ms = (time.time() - start_time) * 1000 + try: + record_tool_usage(tool_name, success, + duration_ms, error, sub_action=sub_action) + except Exception: + _log.debug("record_tool_usage failed", exc_info=True) + + return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper + return decorator diff --git a/MCPForUnity/UnityMcpServer~/src/test_telemetry.py b/MCPForUnity/UnityMcpServer~/src/test_telemetry.py new file mode 100644 index 00000000..145f14e1 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/test_telemetry.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +Test script for Unity MCP Telemetry System +Run this to verify telemetry is working correctly +""" + +import os +from pathlib import Path +import sys + +# Add src to Python path for imports +sys.path.insert(0, str(Path(__file__).parent)) + + +def test_telemetry_basic(): + """Test basic telemetry functionality""" + # Avoid stdout noise in tests + + try: + from telemetry import ( + get_telemetry, record_telemetry, record_milestone, + RecordType, MilestoneType, is_telemetry_enabled + ) + pass + except ImportError as e: + # Silent failure path for tests + return False + + # Test telemetry enabled status + _ = is_telemetry_enabled() + + # Test basic record + try: + record_telemetry(RecordType.VERSION, { + "version": "3.0.2", + "test_run": True + }) + pass + except Exception as e: + # Silent failure path for tests + return False + + # Test milestone recording + try: + is_first = record_milestone(MilestoneType.FIRST_STARTUP, { + "test_mode": True + }) + _ = is_first + except Exception as e: + # Silent failure path for tests + return False + + # Test telemetry collector + try: + collector = get_telemetry() + _ = collector + except Exception as e: + # Silent failure path for tests + return False + + return True + + +def test_telemetry_disabled(): + """Test telemetry with disabled state""" + # Silent for tests + + # Set environment variable to disable telemetry + os.environ["DISABLE_TELEMETRY"] = "true" + + # Re-import to get fresh config + import importlib + import telemetry + importlib.reload(telemetry) + + from telemetry import is_telemetry_enabled, record_telemetry, RecordType + + _ = is_telemetry_enabled() + + if not is_telemetry_enabled(): + pass + + # Test that records are ignored when disabled + record_telemetry(RecordType.USAGE, {"test": "should_be_ignored"}) + pass + + return True + else: + pass + return False + + +def test_data_storage(): + """Test data storage functionality""" + # Silent for tests + + try: + from telemetry import get_telemetry + + collector = get_telemetry() + data_dir = collector.config.data_dir + + _ = (data_dir, collector.config.uuid_file, + collector.config.milestones_file) + + # Check if files exist + if collector.config.uuid_file.exists(): + pass + else: + pass + + if collector.config.milestones_file.exists(): + pass + else: + pass + + return True + + except Exception as e: + # Silent failure path for tests + return False + + +def main(): + """Run all telemetry tests""" + # Silent runner for CI + + tests = [ + test_telemetry_basic, + test_data_storage, + test_telemetry_disabled, + ] + + passed = 0 + failed = 0 + + for test in tests: + try: + if test(): + passed += 1 + pass + else: + failed += 1 + pass + except Exception as e: + failed += 1 + pass + + _ = (passed, failed) + + if failed == 0: + pass + return True + else: + pass + return False + + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) diff --git a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py new file mode 100644 index 00000000..6ede53d3 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py @@ -0,0 +1,60 @@ +""" +MCP Tools package - Auto-discovers and registers all tools in this directory. +""" +import importlib +import logging +from pathlib import Path +import pkgutil + +from mcp.server.fastmcp import FastMCP +from telemetry_decorator import telemetry_tool + +from registry import get_registered_tools, mcp_for_unity_tool + +logger = logging.getLogger("mcp-for-unity-server") + +# Export decorator for easy imports within tools +__all__ = ['register_all_tools', 'mcp_for_unity_tool'] + + +def register_all_tools(mcp: FastMCP): + """ + Auto-discover and register all tools in the tools/ directory. + + Any .py file in this directory with @mcp_for_unity_tool decorated + functions will be automatically registered. + """ + logger.info("Auto-discovering MCP for Unity Server tools...") + # Dynamic import of all modules in this directory + tools_dir = Path(__file__).parent + + for _, module_name, _ in pkgutil.iter_modules([str(tools_dir)]): + # Skip private modules and __init__ + if module_name.startswith('_'): + continue + + try: + importlib.import_module(f'.{module_name}', __package__) + except Exception as e: + logger.warning(f"Failed to import tool module {module_name}: {e}") + + tools = get_registered_tools() + + if not tools: + logger.warning("No MCP tools registered!") + return + + for tool_info in tools: + func = tool_info['func'] + tool_name = tool_info['name'] + description = tool_info['description'] + kwargs = tool_info['kwargs'] + + # Apply the @mcp.tool decorator and telemetry + wrapped = mcp.tool( + name=tool_name, description=description, **kwargs)(func) + wrapped = telemetry_tool(tool_name)(wrapped) + tool_info['func'] = wrapped + logger.info(f"Registered tool: {tool_name} - {description}") + + logger.info(f"Registered {len(tools)} MCP tools") diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py new file mode 100644 index 00000000..5e21d2ce --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py @@ -0,0 +1,83 @@ +""" +Defines the manage_asset tool for interacting with Unity assets. +""" +import asyncio +from typing import Annotated, Any, Literal + +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import async_send_command_with_retry + + +@mcp_for_unity_tool( + description="Performs asset operations (import, create, modify, delete, etc.) in Unity." +) +async def manage_asset( + ctx: Context, + action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."], + path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."], + asset_type: Annotated[str, + "Asset type (e.g., 'Material', 'Folder') - required for 'create'."] | None = None, + properties: Annotated[dict[str, Any], + "Dictionary of properties for 'create'/'modify'."] | None = None, + destination: Annotated[str, + "Target path for 'duplicate'/'move'."] | None = None, + generate_preview: Annotated[bool, + "Generate a preview/thumbnail for the asset when supported."] = False, + search_pattern: Annotated[str, + "Search pattern (e.g., '*.prefab')."] | None = None, + filter_type: Annotated[str, "Filter type for search"] | None = None, + filter_date_after: Annotated[str, + "Date after which to filter"] | None = None, + page_size: Annotated[int, "Page size for pagination"] | None = None, + page_number: Annotated[int, "Page number for pagination"] | None = None +) -> dict[str, Any]: + ctx.info(f"Processing manage_asset: {action}") + # Ensure properties is a dict if None + if properties is None: + properties = {} + + # Coerce numeric inputs defensively + def _coerce_int(value, default=None): + if value is None: + return default + try: + if isinstance(value, bool): + return default + if isinstance(value, int): + return int(value) + s = str(value).strip() + if s.lower() in ("", "none", "null"): + return default + return int(float(s)) + except Exception: + return default + + page_size = _coerce_int(page_size) + page_number = _coerce_int(page_number) + + # Prepare parameters for the C# handler + params_dict = { + "action": action.lower(), + "path": path, + "assetType": asset_type, + "properties": properties, + "destination": destination, + "generatePreview": generate_preview, + "searchPattern": search_pattern, + "filterType": filter_type, + "filterDateAfter": filter_date_after, + "pageSize": page_size, + "pageNumber": page_number + } + + # Remove None values to avoid sending unnecessary nulls + params_dict = {k: v for k, v in params_dict.items() if v is not None} + + # Get the current asyncio event loop + loop = asyncio.get_running_loop() + + # Use centralized async retry helper to avoid blocking the event loop + result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop) + # Return the result obtained from Unity + return result if isinstance(result, dict) else {"success": False, "message": str(result)} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py new file mode 100644 index 00000000..c0de76c2 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py @@ -0,0 +1,57 @@ +from typing import Annotated, Any, Literal + +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from telemetry import is_telemetry_enabled, record_tool_usage +from unity_connection import send_command_with_retry + + +@mcp_for_unity_tool( + description="Controls and queries the Unity editor's state and settings" +) +def manage_editor( + ctx: Context, + action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "get_state", "get_project_root", "get_windows", + "get_active_tool", "get_selection", "get_prefab_stage", "set_active_tool", "add_tag", "remove_tag", "get_tags", "add_layer", "remove_layer", "get_layers"], "Get and update the Unity Editor state."], + wait_for_completion: Annotated[bool, + "Optional. If True, waits for certain actions"] | None = None, + tool_name: Annotated[str, + "Tool name when setting active tool"] | None = None, + tag_name: Annotated[str, + "Tag name when adding and removing tags"] | None = None, + layer_name: Annotated[str, + "Layer name when adding and removing layers"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_editor: {action}") + try: + # Diagnostics: quick telemetry checks + if action == "telemetry_status": + return {"success": True, "telemetry_enabled": is_telemetry_enabled()} + + if action == "telemetry_ping": + record_tool_usage("diagnostic_ping", True, 1.0, None) + return {"success": True, "message": "telemetry ping queued"} + # Prepare parameters, removing None values + params = { + "action": action, + "waitForCompletion": wait_for_completion, + "toolName": tool_name, # Corrected parameter name to match C# + "tagName": tag_name, # Pass tag name + "layerName": layer_name, # Pass layer name + # Add other parameters based on the action being performed + # "width": width, + # "height": height, + # etc. + } + params = {k: v for k, v in params.items() if v is not None} + + # Send command using centralized retry helper + response = send_command_with_retry("manage_editor", params) + + # Preserve structured failure data; unwrap success into a friendlier shape + if isinstance(response, dict) and response.get("success"): + return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")} + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + + except Exception as e: + return {"success": False, "message": f"Python error managing editor: {str(e)}"} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py new file mode 100644 index 00000000..a8ca1609 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py @@ -0,0 +1,145 @@ +from typing import Annotated, Any, Literal + +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +@mcp_for_unity_tool( + description="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data." +) +def manage_gameobject( + ctx: Context, + action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."], + target: Annotated[str, + "GameObject identifier by name or path for modify/delete/component actions"] | None = None, + search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"], + "How to find objects. Used with 'find' and some 'target' lookups."] | None = None, + name: Annotated[str, + "GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead."] | None = None, + tag: Annotated[str, + "Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None, + parent: Annotated[str, + "Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None, + position: Annotated[list[float], + "Position - used for both 'create' (initial position) and 'modify' (change position)"] | None = None, + rotation: Annotated[list[float], + "Rotation - used for both 'create' (initial rotation) and 'modify' (change rotation)"] | None = None, + scale: Annotated[list[float], + "Scale - used for both 'create' (initial scale) and 'modify' (change scale)"] | None = None, + components_to_add: Annotated[list[str], + "List of component names to add"] | None = None, + primitive_type: Annotated[str, + "Primitive type for 'create' action"] | None = None, + save_as_prefab: Annotated[bool, + "If True, saves the created GameObject as a prefab"] | None = None, + prefab_path: Annotated[str, "Path for prefab creation"] | None = None, + prefab_folder: Annotated[str, + "Folder for prefab creation"] | None = None, + # --- Parameters for 'modify' --- + set_active: Annotated[bool, + "If True, sets the GameObject active"] | None = None, + layer: Annotated[str, "Layer name"] | None = None, + components_to_remove: Annotated[list[str], + "List of component names to remove"] | None = None, + component_properties: Annotated[dict[str, dict[str, Any]], + """Dictionary of component names to their properties to set. For example: + `{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject + `{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component + Example set nested property: + - Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`"""] | None = None, + # --- Parameters for 'find' --- + search_term: Annotated[str, + "Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None, + find_all: Annotated[bool, + "If True, finds all GameObjects matching the search term"] | None = None, + search_in_children: Annotated[bool, + "If True, searches in children of the GameObject"] | None = None, + search_inactive: Annotated[bool, + "If True, searches inactive GameObjects"] | None = None, + # -- Component Management Arguments -- + component_name: Annotated[str, + "Component name for 'add_component' and 'remove_component' actions"] | None = None, + # Controls whether serialization of private [SerializeField] fields is included + includeNonPublicSerialized: Annotated[bool, + "Controls whether serialization of private [SerializeField] fields is included"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_gameobject: {action}") + try: + # Validate parameter usage to prevent silent failures + if action == "find": + if name is not None: + return { + "success": False, + "message": "For 'find' action, use 'search_term' parameter, not 'name'. Remove 'name' parameter. Example: search_term='Player', search_method='by_name'" + } + if search_term is None: + return { + "success": False, + "message": "For 'find' action, 'search_term' parameter is required. Use search_term (not 'name') to specify what to find." + } + + if action in ["create", "modify"]: + if search_term is not None: + return { + "success": False, + "message": f"For '{action}' action, use 'name' parameter, not 'search_term'." + } + + # Prepare parameters, removing None values + params = { + "action": action, + "target": target, + "searchMethod": search_method, + "name": name, + "tag": tag, + "parent": parent, + "position": position, + "rotation": rotation, + "scale": scale, + "componentsToAdd": components_to_add, + "primitiveType": primitive_type, + "saveAsPrefab": save_as_prefab, + "prefabPath": prefab_path, + "prefabFolder": prefab_folder, + "setActive": set_active, + "layer": layer, + "componentsToRemove": components_to_remove, + "componentProperties": component_properties, + "searchTerm": search_term, + "findAll": find_all, + "searchInChildren": search_in_children, + "searchInactive": search_inactive, + "componentName": component_name, + "includeNonPublicSerialized": includeNonPublicSerialized + } + params = {k: v for k, v in params.items() if v is not None} + + # --- Handle Prefab Path Logic --- + # Check if 'saveAsPrefab' is explicitly True in params + if action == "create" and params.get("saveAsPrefab"): + if "prefabPath" not in params: + if "name" not in params or not params["name"]: + return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."} + # Use the provided prefab_folder (which has a default) and the name to construct the path + constructed_path = f"{prefab_folder}/{params['name']}.prefab" + # Ensure clean path separators (Unity prefers '/') + params["prefabPath"] = constructed_path.replace("\\", "/") + elif not params["prefabPath"].lower().endswith(".prefab"): + return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"} + # Ensure prefabFolder itself isn't sent if prefabPath was constructed or provided + # The C# side only needs the final prefabPath + params.pop("prefabFolder", None) + # -------------------------------- + + # Use centralized retry helper + response = send_command_with_retry("manage_gameobject", params) + + # Check if the response indicates success + # If the response is not successful, raise an exception with the error message + if isinstance(response, dict) and response.get("success"): + return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")} + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + + except Exception as e: + return {"success": False, "message": f"Python error managing GameObject: {str(e)}"} \ No newline at end of file diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py new file mode 100644 index 00000000..5463614d --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py @@ -0,0 +1,41 @@ +""" +Defines the manage_menu_item tool for executing and reading Unity Editor menu items. +""" +import asyncio +from typing import Annotated, Any, Literal + +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import async_send_command_with_retry + + +@mcp_for_unity_tool( + description="Manage Unity menu items (execute/list/exists). If you're not sure what menu item to use, use the 'list' action to find it before using 'execute'." +) +async def manage_menu_item( + ctx: Context, + action: Annotated[Literal["execute", "list", "exists"], "Read and execute Unity menu items."], + menu_path: Annotated[str, + "Menu path for 'execute' or 'exists' (e.g., 'File/Save Project')"] | None = None, + search: Annotated[str, + "Optional filter string for 'list' (e.g., 'Save')"] | None = None, + refresh: Annotated[bool, + "Optional flag to force refresh of the menu cache when listing"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_menu_item: {action}") + # Prepare parameters for the C# handler + params_dict: dict[str, Any] = { + "action": action, + "menuPath": menu_path, + "search": search, + "refresh": refresh, + } + # Remove None values + params_dict = {k: v for k, v in params_dict.items() if v is not None} + + # Get the current asyncio event loop + loop = asyncio.get_running_loop() + + # Use centralized async retry helper + result = await async_send_command_with_retry("manage_menu_item", params_dict, loop=loop) + return result if isinstance(result, dict) else {"success": False, "message": str(result)} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py new file mode 100644 index 00000000..ea89201c --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py @@ -0,0 +1,58 @@ +from typing import Annotated, Any, Literal + +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +@mcp_for_unity_tool( + description="Bridge for prefab management commands (stage control and creation)." +) +def manage_prefabs( + ctx: Context, + action: Annotated[Literal[ + "open_stage", + "close_stage", + "save_open_stage", + "create_from_gameobject", + ], "Manage prefabs (stage control and creation)."], + prefab_path: Annotated[str, + "Prefab asset path relative to Assets e.g. Assets/Prefabs/favorite.prefab"] | None = None, + mode: Annotated[str, + "Optional prefab stage mode (only 'InIsolation' is currently supported)"] | None = None, + save_before_close: Annotated[bool, + "When true, `close_stage` will save the prefab before exiting the stage."] | None = None, + target: Annotated[str, + "Scene GameObject name required for create_from_gameobject"] | None = None, + allow_overwrite: Annotated[bool, + "Allow replacing an existing prefab at the same path"] | None = None, + search_inactive: Annotated[bool, + "Include inactive objects when resolving the target name"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_prefabs: {action}") + try: + params: dict[str, Any] = {"action": action} + + if prefab_path: + params["prefabPath"] = prefab_path + if mode: + params["mode"] = mode + if save_before_close is not None: + params["saveBeforeClose"] = bool(save_before_close) + if target: + params["target"] = target + if allow_overwrite is not None: + params["allowOverwrite"] = bool(allow_overwrite) + if search_inactive is not None: + params["searchInactive"] = bool(search_inactive) + response = send_command_with_retry("manage_prefabs", params) + + if isinstance(response, dict) and response.get("success"): + return { + "success": True, + "message": response.get("message", "Prefab operation successful."), + "data": response.get("data"), + } + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + except Exception as exc: + return {"success": False, "message": f"Python error managing prefabs: {exc}"} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py new file mode 100644 index 00000000..09494e4a --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py @@ -0,0 +1,56 @@ +from typing import Annotated, Literal, Any + +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +@mcp_for_unity_tool(description="Manage Unity scenes") +def manage_scene( + ctx: Context, + action: Annotated[Literal["create", "load", "save", "get_hierarchy", "get_active", "get_build_settings"], "Perform CRUD operations on Unity scenes."], + name: Annotated[str, + "Scene name. Not required get_active/get_build_settings"] | None = None, + path: Annotated[str, + "Asset path for scene operations (default: 'Assets/')"] | None = None, + build_index: Annotated[int, + "Build index for load/build settings actions"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_scene: {action}") + try: + # Coerce numeric inputs defensively + def _coerce_int(value, default=None): + if value is None: + return default + try: + if isinstance(value, bool): + return default + if isinstance(value, int): + return int(value) + s = str(value).strip() + if s.lower() in ("", "none", "null"): + return default + return int(float(s)) + except Exception: + return default + + coerced_build_index = _coerce_int(build_index, default=None) + + params = {"action": action} + if name: + params["name"] = name + if path: + params["path"] = path + if coerced_build_index is not None: + params["buildIndex"] = coerced_build_index + + # Use centralized retry helper + response = send_command_with_retry("manage_scene", params) + + # Preserve structured failure data; unwrap success into a friendlier shape + if isinstance(response, dict) and response.get("success"): + return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")} + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + + except Exception as e: + return {"success": False, "message": f"Python error managing scene: {str(e)}"} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py new file mode 100644 index 00000000..cad6a88c --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py @@ -0,0 +1,552 @@ +import base64 +import os +from typing import Annotated, Any, Literal +from urllib.parse import urlparse, unquote + +from mcp.server.fastmcp import FastMCP, Context + +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +def _split_uri(uri: str) -> tuple[str, str]: + """Split an incoming URI or path into (name, directory) suitable for Unity. + + Rules: + - unity://path/Assets/... → keep as Assets-relative (after decode/normalize) + - file://... → percent-decode, normalize, strip host and leading slashes, + then, if any 'Assets' segment exists, return path relative to that 'Assets' root. + Otherwise, fall back to original name/dir behavior. + - plain paths → decode/normalize separators; if they contain an 'Assets' segment, + return relative to 'Assets'. + """ + raw_path: str + if uri.startswith("unity://path/"): + raw_path = uri[len("unity://path/"):] + elif uri.startswith("file://"): + parsed = urlparse(uri) + host = (parsed.netloc or "").strip() + p = parsed.path or "" + # UNC: file://server/share/... -> //server/share/... + if host and host.lower() != "localhost": + p = f"//{host}{p}" + # Use percent-decoded path, preserving leading slashes + raw_path = unquote(p) + else: + raw_path = uri + + # Percent-decode any residual encodings and normalize separators + raw_path = unquote(raw_path).replace("\\", "/") + # Strip leading slash only for Windows drive-letter forms like "/C:/..." + if os.name == "nt" and len(raw_path) >= 3 and raw_path[0] == "/" and raw_path[2] == ":": + raw_path = raw_path[1:] + + # Normalize path (collapse ../, ./) + norm = os.path.normpath(raw_path).replace("\\", "/") + + # If an 'Assets' segment exists, compute path relative to it (case-insensitive) + parts = [p for p in norm.split("/") if p not in ("", ".")] + idx = next((i for i, seg in enumerate(parts) + if seg.lower() == "assets"), None) + assets_rel = "/".join(parts[idx:]) if idx is not None else None + + effective_path = assets_rel if assets_rel else norm + # For POSIX absolute paths outside Assets, drop the leading '/' + # to return a clean relative-like directory (e.g., '/tmp' -> 'tmp'). + if effective_path.startswith("/"): + effective_path = effective_path[1:] + + name = os.path.splitext(os.path.basename(effective_path))[0] + directory = os.path.dirname(effective_path) + return name, directory + + +@mcp_for_unity_tool(description=( + """Apply small text edits to a C# script identified by URI. + IMPORTANT: This tool replaces EXACT character positions. Always verify content at target lines/columns BEFORE editing! + RECOMMENDED WORKFLOW: + 1. First call resources/read with start_line/line_count to verify exact content + 2. Count columns carefully (or use find_in_file to locate patterns) + 3. Apply your edit with precise coordinates + 4. Consider script_apply_edits with anchors for safer pattern-based replacements + Notes: + - For method/class operations, use script_apply_edits (safer, structured edits) + - For pattern-based replacements, consider anchor operations in script_apply_edits + - Lines, columns are 1-indexed + - Tabs count as 1 column""" +)) +def apply_text_edits( + ctx: Context, + uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."], + edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script, i.e. a list of {startLine,startCol,endLine,endCol,newText} (1-indexed!)"], + precondition_sha256: Annotated[str, + "Optional SHA256 of the script to edit, used to prevent concurrent edits"] | None = None, + strict: Annotated[bool, + "Optional strict flag, used to enforce strict mode"] | None = None, + options: Annotated[dict[str, Any], + "Optional options, used to pass additional options to the script editor"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing apply_text_edits: {uri}") + name, directory = _split_uri(uri) + + # Normalize common aliases/misuses for resilience: + # - Accept LSP-style range objects: {range:{start:{line,character}, end:{...}}, newText|text} + # - Accept index ranges as a 2-int array: {range:[startIndex,endIndex], text} + # If normalization is required, read current contents to map indices -> 1-based line/col. + def _needs_normalization(arr: list[dict[str, Any]]) -> bool: + for e in arr or []: + if ("startLine" not in e) or ("startCol" not in e) or ("endLine" not in e) or ("endCol" not in e) or ("newText" not in e and "text" in e): + return True + return False + + normalized_edits: list[dict[str, Any]] = [] + warnings: list[str] = [] + if _needs_normalization(edits): + # Read file to support index->line/col conversion when needed + read_resp = send_command_with_retry("manage_script", { + "action": "read", + "name": name, + "path": directory, + }) + if not (isinstance(read_resp, dict) and read_resp.get("success")): + return read_resp if isinstance(read_resp, dict) else {"success": False, "message": str(read_resp)} + data = read_resp.get("data", {}) + contents = data.get("contents") + if not contents and data.get("contentsEncoded"): + try: + contents = base64.b64decode(data.get("encodedContents", "").encode( + "utf-8")).decode("utf-8", "replace") + except Exception: + contents = contents or "" + + # Helper to map 0-based character index to 1-based line/col + def line_col_from_index(idx: int) -> tuple[int, int]: + if idx <= 0: + return 1, 1 + # Count lines up to idx and position within line + nl_count = contents.count("\n", 0, idx) + line = nl_count + 1 + last_nl = contents.rfind("\n", 0, idx) + col = (idx - (last_nl + 1)) + 1 if last_nl >= 0 else idx + 1 + return line, col + + for e in edits or []: + e2 = dict(e) + # Map text->newText if needed + if "newText" not in e2 and "text" in e2: + e2["newText"] = e2.pop("text") + + if "startLine" in e2 and "startCol" in e2 and "endLine" in e2 and "endCol" in e2: + # Guard: explicit fields must be 1-based. + zero_based = False + for k in ("startLine", "startCol", "endLine", "endCol"): + try: + if int(e2.get(k, 1)) < 1: + zero_based = True + except Exception: + pass + if zero_based: + if strict: + return {"success": False, "code": "zero_based_explicit_fields", "message": "Explicit line/col fields are 1-based; received zero-based.", "data": {"normalizedEdits": normalized_edits}} + # Normalize by clamping to 1 and warn + for k in ("startLine", "startCol", "endLine", "endCol"): + try: + if int(e2.get(k, 1)) < 1: + e2[k] = 1 + except Exception: + pass + warnings.append( + "zero_based_explicit_fields_normalized") + normalized_edits.append(e2) + continue + + rng = e2.get("range") + if isinstance(rng, dict): + # LSP style: 0-based + s = rng.get("start", {}) + t = rng.get("end", {}) + e2["startLine"] = int(s.get("line", 0)) + 1 + e2["startCol"] = int(s.get("character", 0)) + 1 + e2["endLine"] = int(t.get("line", 0)) + 1 + e2["endCol"] = int(t.get("character", 0)) + 1 + e2.pop("range", None) + normalized_edits.append(e2) + continue + if isinstance(rng, (list, tuple)) and len(rng) == 2: + try: + a = int(rng[0]) + b = int(rng[1]) + if b < a: + a, b = b, a + sl, sc = line_col_from_index(a) + el, ec = line_col_from_index(b) + e2["startLine"] = sl + e2["startCol"] = sc + e2["endLine"] = el + e2["endCol"] = ec + e2.pop("range", None) + normalized_edits.append(e2) + continue + except Exception: + pass + # Could not normalize this edit + return { + "success": False, + "code": "missing_field", + "message": "apply_text_edits requires startLine/startCol/endLine/endCol/newText or a normalizable 'range'", + "data": {"expected": ["startLine", "startCol", "endLine", "endCol", "newText"], "got": e} + } + else: + # Even when edits appear already in explicit form, validate 1-based coordinates. + normalized_edits = [] + for e in edits or []: + e2 = dict(e) + has_all = all(k in e2 for k in ( + "startLine", "startCol", "endLine", "endCol")) + if has_all: + zero_based = False + for k in ("startLine", "startCol", "endLine", "endCol"): + try: + if int(e2.get(k, 1)) < 1: + zero_based = True + except Exception: + pass + if zero_based: + if strict: + return {"success": False, "code": "zero_based_explicit_fields", "message": "Explicit line/col fields are 1-based; received zero-based.", "data": {"normalizedEdits": [e2]}} + for k in ("startLine", "startCol", "endLine", "endCol"): + try: + if int(e2.get(k, 1)) < 1: + e2[k] = 1 + except Exception: + pass + if "zero_based_explicit_fields_normalized" not in warnings: + warnings.append( + "zero_based_explicit_fields_normalized") + normalized_edits.append(e2) + + # Preflight: detect overlapping ranges among normalized line/col spans + def _pos_tuple(e: dict[str, Any], key_start: bool) -> tuple[int, int]: + return ( + int(e.get("startLine", 1)) if key_start else int( + e.get("endLine", 1)), + int(e.get("startCol", 1)) if key_start else int( + e.get("endCol", 1)), + ) + + def _le(a: tuple[int, int], b: tuple[int, int]) -> bool: + return a[0] < b[0] or (a[0] == b[0] and a[1] <= b[1]) + + # Consider only true replace ranges (non-zero length). Pure insertions (zero-width) don't overlap. + spans = [] + for e in normalized_edits or []: + try: + s = _pos_tuple(e, True) + t = _pos_tuple(e, False) + if s != t: + spans.append((s, t)) + except Exception: + # If coordinates missing or invalid, let the server validate later + pass + + if spans: + spans_sorted = sorted(spans, key=lambda p: (p[0][0], p[0][1])) + for i in range(1, len(spans_sorted)): + prev_end = spans_sorted[i-1][1] + curr_start = spans_sorted[i][0] + # Overlap if prev_end > curr_start (strict), i.e., not prev_end <= curr_start + if not _le(prev_end, curr_start): + conflicts = [{ + "startA": {"line": spans_sorted[i-1][0][0], "col": spans_sorted[i-1][0][1]}, + "endA": {"line": spans_sorted[i-1][1][0], "col": spans_sorted[i-1][1][1]}, + "startB": {"line": spans_sorted[i][0][0], "col": spans_sorted[i][0][1]}, + "endB": {"line": spans_sorted[i][1][0], "col": spans_sorted[i][1][1]}, + }] + return {"success": False, "code": "overlap", "data": {"status": "overlap", "conflicts": conflicts}} + + # Note: Do not auto-compute precondition if missing; callers should supply it + # via mcp__unity__get_sha or a prior read. This avoids hidden extra calls and + # preserves existing call-count expectations in clients/tests. + + # Default options: for multi-span batches, prefer atomic to avoid mid-apply imbalance + opts: dict[str, Any] = dict(options or {}) + try: + if len(normalized_edits) > 1 and "applyMode" not in opts: + opts["applyMode"] = "atomic" + except Exception: + pass + # Support optional debug preview for span-by-span simulation without write + if opts.get("debug_preview"): + try: + import difflib + # Apply locally to preview final result + lines = [] + # Build an indexable original from a read if we normalized from read; otherwise skip + prev = "" + # We cannot guarantee file contents here without a read; return normalized spans only + return { + "success": True, + "message": "Preview only (no write)", + "data": { + "normalizedEdits": normalized_edits, + "preview": True + } + } + except Exception as e: + return {"success": False, "code": "preview_failed", "message": f"debug_preview failed: {e}", "data": {"normalizedEdits": normalized_edits}} + + params = { + "action": "apply_text_edits", + "name": name, + "path": directory, + "edits": normalized_edits, + "precondition_sha256": precondition_sha256, + "options": opts, + } + params = {k: v for k, v in params.items() if v is not None} + resp = send_command_with_retry("manage_script", params) + if isinstance(resp, dict): + data = resp.setdefault("data", {}) + data.setdefault("normalizedEdits", normalized_edits) + if warnings: + data.setdefault("warnings", warnings) + if resp.get("success") and (options or {}).get("force_sentinel_reload"): + # Optional: flip sentinel via menu if explicitly requested + try: + import threading + import time + import json + import glob + import os + + def _latest_status() -> dict | None: + try: + files = sorted(glob.glob(os.path.expanduser( + "~/.unity-mcp/unity-mcp-status-*.json")), key=os.path.getmtime, reverse=True) + if not files: + return None + with open(files[0], "r") as f: + return json.loads(f.read()) + except Exception: + return None + + def _flip_async(): + try: + time.sleep(0.1) + st = _latest_status() + if st and st.get("reloading"): + return + send_command_with_retry( + "execute_menu_item", + {"menuPath": "MCP/Flip Reload Sentinel"}, + max_retries=0, + retry_ms=0, + ) + except Exception: + pass + threading.Thread(target=_flip_async, daemon=True).start() + except Exception: + pass + return resp + return resp + return {"success": False, "message": str(resp)} + + +@mcp_for_unity_tool(description=("Create a new C# script at the given project path.")) +def create_script( + ctx: Context, + path: Annotated[str, "Path under Assets/ to create the script at, e.g., 'Assets/Scripts/My.cs'"], + contents: Annotated[str, "Contents of the script to create. Note, this is Base64 encoded over transport."], + script_type: Annotated[str, "Script type (e.g., 'C#')"] | None = None, + namespace: Annotated[str, "Namespace for the script"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing create_script: {path}") + name = os.path.splitext(os.path.basename(path))[0] + directory = os.path.dirname(path) + # Local validation to avoid round-trips on obviously bad input + norm_path = os.path.normpath( + (path or "").replace("\\", "/")).replace("\\", "/") + if not directory or directory.split("/")[0].lower() != "assets": + return {"success": False, "code": "path_outside_assets", "message": f"path must be under 'Assets/'; got '{path}'."} + if ".." in norm_path.split("/") or norm_path.startswith("/"): + return {"success": False, "code": "bad_path", "message": "path must not contain traversal or be absolute."} + if not name: + return {"success": False, "code": "bad_path", "message": "path must include a script file name."} + if not norm_path.lower().endswith(".cs"): + return {"success": False, "code": "bad_extension", "message": "script file must end with .cs."} + params: dict[str, Any] = { + "action": "create", + "name": name, + "path": directory, + "namespace": namespace, + "scriptType": script_type, + } + if contents: + params["encodedContents"] = base64.b64encode( + contents.encode("utf-8")).decode("utf-8") + params["contentsEncoded"] = True + params = {k: v for k, v in params.items() if v is not None} + resp = send_command_with_retry("manage_script", params) + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + + +@mcp_for_unity_tool(description=("Delete a C# script by URI or Assets-relative path.")) +def delete_script( + ctx: Context, + uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] +) -> dict[str, Any]: + """Delete a C# script by URI.""" + ctx.info(f"Processing delete_script: {uri}") + name, directory = _split_uri(uri) + if not directory or directory.split("/")[0].lower() != "assets": + return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} + params = {"action": "delete", "name": name, "path": directory} + resp = send_command_with_retry("manage_script", params) + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + + +@mcp_for_unity_tool(description=("Validate a C# script and return diagnostics.")) +def validate_script( + ctx: Context, + uri: Annotated[str, "URI of the script to validate under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."], + level: Annotated[Literal['basic', 'standard'], + "Validation level"] = "basic", + include_diagnostics: Annotated[bool, + "Include full diagnostics and summary"] = False +) -> dict[str, Any]: + ctx.info(f"Processing validate_script: {uri}") + name, directory = _split_uri(uri) + if not directory or directory.split("/")[0].lower() != "assets": + return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} + if level not in ("basic", "standard"): + return {"success": False, "code": "bad_level", "message": "level must be 'basic' or 'standard'."} + params = { + "action": "validate", + "name": name, + "path": directory, + "level": level, + } + resp = send_command_with_retry("manage_script", params) + if isinstance(resp, dict) and resp.get("success"): + diags = resp.get("data", {}).get("diagnostics", []) or [] + warnings = sum(1 for d in diags if str( + d.get("severity", "")).lower() == "warning") + errors = sum(1 for d in diags if str( + d.get("severity", "")).lower() in ("error", "fatal")) + if include_diagnostics: + return {"success": True, "data": {"diagnostics": diags, "summary": {"warnings": warnings, "errors": errors}}} + return {"success": True, "data": {"warnings": warnings, "errors": errors}} + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + + +@mcp_for_unity_tool(description=("Compatibility router for legacy script operations. Prefer apply_text_edits (ranges) or script_apply_edits (structured) for edits.")) +def manage_script( + ctx: Context, + action: Annotated[Literal['create', 'read', 'delete'], "Perform CRUD operations on C# scripts."], + name: Annotated[str, "Script name (no .cs extension)", "Name of the script to create"], + path: Annotated[str, "Asset path (default: 'Assets/')", "Path under Assets/ to create the script at, e.g., 'Assets/Scripts/My.cs'"], + contents: Annotated[str, "Contents of the script to create", + "C# code for 'create'/'update'"] | None = None, + script_type: Annotated[str, "Script type (e.g., 'C#')", + "Type hint (e.g., 'MonoBehaviour')"] | None = None, + namespace: Annotated[str, "Namespace for the script"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_script: {action}") + try: + # Prepare parameters for Unity + params = { + "action": action, + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + } + + # Base64 encode the contents if they exist to avoid JSON escaping issues + if contents: + if action == 'create': + params["encodedContents"] = base64.b64encode( + contents.encode('utf-8')).decode('utf-8') + params["contentsEncoded"] = True + else: + params["contents"] = contents + + params = {k: v for k, v in params.items() if v is not None} + + response = send_command_with_retry("manage_script", params) + + if isinstance(response, dict): + if response.get("success"): + if response.get("data", {}).get("contentsEncoded"): + decoded_contents = base64.b64decode( + response["data"]["encodedContents"]).decode('utf-8') + response["data"]["contents"] = decoded_contents + del response["data"]["encodedContents"] + del response["data"]["contentsEncoded"] + + return { + "success": True, + "message": response.get("message", "Operation successful."), + "data": response.get("data"), + } + return response + + return {"success": False, "message": str(response)} + + except Exception as e: + return { + "success": False, + "message": f"Python error managing script: {str(e)}", + } + + +@mcp_for_unity_tool(description=( + """Get manage_script capabilities (supported ops, limits, and guards). + Returns: + - ops: list of supported structured ops + - text_ops: list of supported text ops + - max_edit_payload_bytes: server edit payload cap + - guards: header/using guard enabled flag""" +)) +def manage_script_capabilities(ctx: Context) -> dict[str, Any]: + ctx.info("Processing manage_script_capabilities") + try: + # Keep in sync with server/Editor ManageScript implementation + ops = [ + "replace_class", "delete_class", "replace_method", "delete_method", + "insert_method", "anchor_insert", "anchor_delete", "anchor_replace" + ] + text_ops = ["replace_range", "regex_replace", "prepend", "append"] + # Match ManageScript.MaxEditPayloadBytes if exposed; hardcode a sensible default fallback + max_edit_payload_bytes = 256 * 1024 + guards = {"using_guard": True} + extras = {"get_sha": True} + return {"success": True, "data": { + "ops": ops, + "text_ops": text_ops, + "max_edit_payload_bytes": max_edit_payload_bytes, + "guards": guards, + "extras": extras, + }} + except Exception as e: + return {"success": False, "error": f"capabilities error: {e}"} + + +@mcp_for_unity_tool(description="Get SHA256 and basic metadata for a Unity C# script without returning file contents") +def get_sha( + ctx: Context, + uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] +) -> dict[str, Any]: + ctx.info(f"Processing get_sha: {uri}") + try: + name, directory = _split_uri(uri) + params = {"action": "get_sha", "name": name, "path": directory} + resp = send_command_with_retry("manage_script", params) + if isinstance(resp, dict) and resp.get("success"): + data = resp.get("data", {}) + minimal = {"sha256": data.get( + "sha256"), "lengthBytes": data.get("lengthBytes")} + return {"success": True, "data": minimal} + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + except Exception as e: + return {"success": False, "message": f"get_sha error: {e}"} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py new file mode 100644 index 00000000..9c199661 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py @@ -0,0 +1,60 @@ +import base64 +from typing import Annotated, Any, Literal + +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +@mcp_for_unity_tool( + description="Manages shader scripts in Unity (create, read, update, delete)." +) +def manage_shader( + ctx: Context, + action: Annotated[Literal['create', 'read', 'update', 'delete'], "Perform CRUD operations on shader scripts."], + name: Annotated[str, "Shader name (no .cs extension)"], + path: Annotated[str, "Asset path (default: \"Assets/\")"], + contents: Annotated[str, + "Shader code for 'create'/'update'"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing manage_shader: {action}") + try: + # Prepare parameters for Unity + params = { + "action": action, + "name": name, + "path": path, + } + + # Base64 encode the contents if they exist to avoid JSON escaping issues + if contents is not None: + if action in ['create', 'update']: + # Encode content for safer transmission + params["encodedContents"] = base64.b64encode( + contents.encode('utf-8')).decode('utf-8') + params["contentsEncoded"] = True + else: + params["contents"] = contents + + # Remove None values so they don't get sent as null + params = {k: v for k, v in params.items() if v is not None} + + # Send command via centralized retry helper + response = send_command_with_retry("manage_shader", params) + + # Process response from Unity + if isinstance(response, dict) and response.get("success"): + # If the response contains base64 encoded content, decode it + if response.get("data", {}).get("contentsEncoded"): + decoded_contents = base64.b64decode( + response["data"]["encodedContents"]).decode('utf-8') + response["data"]["contents"] = decoded_contents + del response["data"]["encodedContents"] + del response["data"]["contentsEncoded"] + + return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} + return response if isinstance(response, dict) else {"success": False, "message": str(response)} + + except Exception as e: + # Handle Python-side errors (e.g., connection issues) + return {"success": False, "message": f"Python error managing shader: {str(e)}"} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/read_console.py b/MCPForUnity/UnityMcpServer~/src/tools/read_console.py new file mode 100644 index 00000000..5fc9a096 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/read_console.py @@ -0,0 +1,87 @@ +""" +Defines the read_console tool for accessing Unity Editor console messages. +""" +from typing import Annotated, Any, Literal + +from mcp.server.fastmcp import Context +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +@mcp_for_unity_tool( + description="Gets messages from or clears the Unity Editor console." +) +def read_console( + ctx: Context, + action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."], + types: Annotated[list[Literal['error', 'warning', + 'log', 'all']], "Message types to get"] | None = None, + count: Annotated[int, "Max messages to return"] | None = None, + filter_text: Annotated[str, "Text filter for messages"] | None = None, + since_timestamp: Annotated[str, + "Get messages after this timestamp (ISO 8601)"] | None = None, + format: Annotated[Literal['plain', 'detailed', + 'json'], "Output format"] | None = None, + include_stacktrace: Annotated[bool, + "Include stack traces in output"] | None = None +) -> dict[str, Any]: + ctx.info(f"Processing read_console: {action}") + # Set defaults if values are None + action = action if action is not None else 'get' + types = types if types is not None else ['error', 'warning', 'log'] + format = format if format is not None else 'detailed' + include_stacktrace = include_stacktrace if include_stacktrace is not None else True + + # Normalize action if it's a string + if isinstance(action, str): + action = action.lower() + + # Coerce count defensively (string/float -> int) + def _coerce_int(value, default=None): + if value is None: + return default + try: + if isinstance(value, bool): + return default + if isinstance(value, int): + return int(value) + s = str(value).strip() + if s.lower() in ("", "none", "null"): + return default + return int(float(s)) + except Exception: + return default + + count = _coerce_int(count) + + # Prepare parameters for the C# handler + params_dict = { + "action": action, + "types": types, + "count": count, + "filterText": filter_text, + "sinceTimestamp": since_timestamp, + "format": format.lower() if isinstance(format, str) else format, + "includeStacktrace": include_stacktrace + } + + # Remove None values unless it's 'count' (as None might mean 'all') + params_dict = {k: v for k, v in params_dict.items() + if v is not None or k == 'count'} + + # Add count back if it was None, explicitly sending null might be important for C# logic + if 'count' not in params_dict: + params_dict['count'] = None + + # Use centralized retry helper + resp = send_command_with_retry("read_console", params_dict) + if isinstance(resp, dict) and resp.get("success") and not include_stacktrace: + # Strip stacktrace fields from returned lines if present + try: + lines = resp.get("data", {}).get("lines", []) + for line in lines: + if isinstance(line, dict) and "stacktrace" in line: + line.pop("stacktrace", None) + except Exception: + pass + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py b/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py new file mode 100644 index 00000000..a8398f75 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py @@ -0,0 +1,392 @@ +""" +Resource wrapper tools so clients that do not expose MCP resources primitives +can still list and read files via normal tools. These call into the same +safe path logic (re-implemented here to avoid importing server.py). +""" +import fnmatch +import hashlib +import os +from pathlib import Path +import re +from typing import Annotated, Any +from urllib.parse import urlparse, unquote + +from mcp.server.fastmcp import Context + +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +def _coerce_int(value: Any, default: int | None = None, minimum: int | None = None) -> int | None: + """Safely coerce various inputs (str/float/etc.) to an int. + Returns default on failure; clamps to minimum when provided. + """ + if value is None: + return default + try: + # Avoid treating booleans as ints implicitly + if isinstance(value, bool): + return default + if isinstance(value, int): + result = int(value) + else: + s = str(value).strip() + if s.lower() in ("", "none", "null"): + return default + # Allow "10.0" or similar inputs + result = int(float(s)) + if minimum is not None and result < minimum: + return minimum + return result + except Exception: + return default + + +def _resolve_project_root(override: str | None) -> Path: + # 1) Explicit override + if override: + pr = Path(override).expanduser().resolve() + if (pr / "Assets").exists(): + return pr + # 2) Environment + env = os.environ.get("UNITY_PROJECT_ROOT") + if env: + env_path = Path(env).expanduser() + # If UNITY_PROJECT_ROOT is relative, resolve against repo root (cwd's repo) instead of src dir + pr = (Path.cwd( + ) / env_path).resolve() if not env_path.is_absolute() else env_path.resolve() + if (pr / "Assets").exists(): + return pr + # 3) Ask Unity via manage_editor.get_project_root + try: + resp = send_command_with_retry( + "manage_editor", {"action": "get_project_root"}) + if isinstance(resp, dict) and resp.get("success"): + pr = Path(resp.get("data", {}).get( + "projectRoot", "")).expanduser().resolve() + if pr and (pr / "Assets").exists(): + return pr + except Exception: + pass + + # 4) Walk up from CWD to find a Unity project (Assets + ProjectSettings) + cur = Path.cwd().resolve() + for _ in range(6): + if (cur / "Assets").exists() and (cur / "ProjectSettings").exists(): + return cur + if cur.parent == cur: + break + cur = cur.parent + # 5) Search downwards (shallow) from repo root for first folder with Assets + ProjectSettings + try: + import os as _os + root = Path.cwd().resolve() + max_depth = 3 + for dirpath, dirnames, _ in _os.walk(root): + rel = Path(dirpath).resolve() + try: + depth = len(rel.relative_to(root).parts) + except Exception: + # Unrelated mount/permission edge; skip deeper traversal + dirnames[:] = [] + continue + if depth > max_depth: + # Prune deeper traversal + dirnames[:] = [] + continue + if (rel / "Assets").exists() and (rel / "ProjectSettings").exists(): + return rel + except Exception: + pass + # 6) Fallback: CWD + return Path.cwd().resolve() + + +def _resolve_safe_path_from_uri(uri: str, project: Path) -> Path | None: + raw: str | None = None + if uri.startswith("unity://path/"): + raw = uri[len("unity://path/"):] + elif uri.startswith("file://"): + parsed = urlparse(uri) + raw = unquote(parsed.path or "") + # On Windows, urlparse('file:///C:/x') -> path='/C:/x'. Strip the leading slash for drive letters. + try: + import os as _os + if _os.name == "nt" and raw.startswith("/") and re.match(r"^/[A-Za-z]:/", raw): + raw = raw[1:] + # UNC paths: file://server/share -> netloc='server', path='/share'. Treat as \\\\server/share + if _os.name == "nt" and parsed.netloc: + raw = f"//{parsed.netloc}{raw}" + except Exception: + pass + elif uri.startswith("Assets/"): + raw = uri + if raw is None: + return None + # Normalize separators early + raw = raw.replace("\\", "/") + p = (project / raw).resolve() + try: + p.relative_to(project) + except ValueError: + return None + return p + + +@mcp_for_unity_tool(description=("List project URIs (unity://path/...) under a folder (default: Assets). Only .cs files are returned by default; always appends unity://spec/script-edits.\n")) +async def list_resources( + ctx: Context, + pattern: Annotated[str, "Glob, default is *.cs"] | None = "*.cs", + under: Annotated[str, + "Folder under project root, default is Assets"] = "Assets", + limit: Annotated[int, "Page limit"] = 200, + project_root: Annotated[str, "Project path"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing list_resources: {pattern}") + try: + project = _resolve_project_root(project_root) + base = (project / under).resolve() + try: + base.relative_to(project) + except ValueError: + return {"success": False, "error": "Base path must be under project root"} + # Enforce listing only under Assets + try: + base.relative_to(project / "Assets") + except ValueError: + return {"success": False, "error": "Listing is restricted to Assets/"} + + matches: list[str] = [] + limit_int = _coerce_int(limit, default=200, minimum=1) + for p in base.rglob("*"): + if not p.is_file(): + continue + # Resolve symlinks and ensure the real path stays under project/Assets + try: + rp = p.resolve() + rp.relative_to(project / "Assets") + except Exception: + continue + # Enforce .cs extension regardless of provided pattern + if p.suffix.lower() != ".cs": + continue + if pattern and not fnmatch.fnmatch(p.name, pattern): + continue + rel = p.relative_to(project).as_posix() + matches.append(f"unity://path/{rel}") + if len(matches) >= max(1, limit_int): + break + + # Always include the canonical spec resource so NL clients can discover it + if "unity://spec/script-edits" not in matches: + matches.append("unity://spec/script-edits") + + return {"success": True, "data": {"uris": matches, "count": len(matches)}} + except Exception as e: + return {"success": False, "error": str(e)} + + +@mcp_for_unity_tool(description=("Reads a resource by unity://path/... URI with optional slicing.")) +async def read_resource( + ctx: Context, + uri: Annotated[str, "The resource URI to read under Assets/"], + start_line: Annotated[int, + "The starting line number (0-based)"] | None = None, + line_count: Annotated[int, + "The number of lines to read"] | None = None, + head_bytes: Annotated[int, + "The number of bytes to read from the start of the file"] | None = None, + tail_lines: Annotated[int, + "The number of lines to read from the end of the file"] | None = None, + project_root: Annotated[str, + "The project root directory"] | None = None, + request: Annotated[str, "The request ID"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing read_resource: {uri}") + try: + # Serve the canonical spec directly when requested (allow bare or with scheme) + if uri in ("unity://spec/script-edits", "spec/script-edits", "script-edits"): + spec_json = ( + '{\n' + ' "name": "Unity MCP - Script Edits v1",\n' + ' "target_tool": "script_apply_edits",\n' + ' "canonical_rules": {\n' + ' "always_use": ["op","className","methodName","replacement","afterMethodName","beforeMethodName"],\n' + ' "never_use": ["new_method","anchor_method","content","newText"],\n' + ' "defaults": {\n' + ' "className": "\u2190 server will default to \'name\' when omitted",\n' + ' "position": "end"\n' + ' }\n' + ' },\n' + ' "ops": [\n' + ' {"op":"replace_method","required":["className","methodName","replacement"],"optional":["returnType","parametersSignature","attributesContains"],"examples":[{"note":"match overload by signature","parametersSignature":"(int a, string b)"},{"note":"ensure attributes retained","attributesContains":"ContextMenu"}]},\n' + ' {"op":"insert_method","required":["className","replacement"],"position":{"enum":["start","end","after","before"],"after_requires":"afterMethodName","before_requires":"beforeMethodName"}},\n' + ' {"op":"delete_method","required":["className","methodName"]},\n' + ' {"op":"anchor_insert","required":["anchor","text"],"notes":"regex; position=before|after"}\n' + ' ],\n' + ' "apply_text_edits_recipe": {\n' + ' "step1_read": { "tool": "resources/read", "args": {"uri": "unity://path/Assets/Scripts/Interaction/SmartReach.cs"} },\n' + ' "step2_apply": {\n' + ' "tool": "manage_script",\n' + ' "args": {\n' + ' "action": "apply_text_edits",\n' + ' "name": "SmartReach", "path": "Assets/Scripts/Interaction",\n' + ' "edits": [{"startLine": 42, "startCol": 1, "endLine": 42, "endCol": 1, "newText": "[MyAttr]\\n"}],\n' + ' "precondition_sha256": "",\n' + ' "options": {"refresh": "immediate", "validate": "standard"}\n' + ' }\n' + ' },\n' + ' "note": "newText is for apply_text_edits ranges only; use replacement in script_apply_edits ops."\n' + ' },\n' + ' "examples": [\n' + ' {\n' + ' "title": "Replace a method",\n' + ' "args": {\n' + ' "name": "SmartReach",\n' + ' "path": "Assets/Scripts/Interaction",\n' + ' "edits": [\n' + ' {"op":"replace_method","className":"SmartReach","methodName":"HasTarget","replacement":"public bool HasTarget() { return currentTarget != null; }"}\n' + ' ],\n' + ' "options": { "validate": "standard", "refresh": "immediate" }\n' + ' }\n' + ' },\n' + ' {\n' + ' "title": "Insert a method after another",\n' + ' "args": {\n' + ' "name": "SmartReach",\n' + ' "path": "Assets/Scripts/Interaction",\n' + ' "edits": [\n' + ' {"op":"insert_method","className":"SmartReach","replacement":"public void PrintSeries() { Debug.Log(seriesName); }","position":"after","afterMethodName":"GetCurrentTarget"}\n' + ' ]\n' + ' }\n' + ' }\n' + ' ]\n' + '}\n' + ) + sha = hashlib.sha256(spec_json.encode("utf-8")).hexdigest() + return {"success": True, "data": {"text": spec_json, "metadata": {"sha256": sha}}} + + project = _resolve_project_root(project_root) + p = _resolve_safe_path_from_uri(uri, project) + if not p or not p.exists() or not p.is_file(): + return {"success": False, "error": f"Resource not found: {uri}"} + try: + p.relative_to(project / "Assets") + except ValueError: + return {"success": False, "error": "Read restricted to Assets/"} + # Natural-language convenience: request like "last 120 lines", "first 200 lines", + # "show 40 lines around MethodName", etc. + if request: + req = request.strip().lower() + m = re.search(r"last\s+(\d+)\s+lines", req) + if m: + tail_lines = int(m.group(1)) + m = re.search(r"first\s+(\d+)\s+lines", req) + if m: + start_line = 1 + line_count = int(m.group(1)) + m = re.search(r"first\s+(\d+)\s*bytes", req) + if m: + head_bytes = int(m.group(1)) + m = re.search( + r"show\s+(\d+)\s+lines\s+around\s+([A-Za-z_][A-Za-z0-9_]*)", req) + if m: + window = int(m.group(1)) + method = m.group(2) + # naive search for method header to get a line number + text_all = p.read_text(encoding="utf-8") + lines_all = text_all.splitlines() + pat = re.compile( + rf"^\s*(?:\[[^\]]+\]\s*)*(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial).*?\b{re.escape(method)}\s*\(", re.MULTILINE) + hit_line = None + for i, line in enumerate(lines_all, start=1): + if pat.search(line): + hit_line = i + break + if hit_line: + half = max(1, window // 2) + start_line = max(1, hit_line - half) + line_count = window + + # Coerce numeric inputs defensively (string/float -> int) + start_line = _coerce_int(start_line) + line_count = _coerce_int(line_count) + head_bytes = _coerce_int(head_bytes, minimum=1) + tail_lines = _coerce_int(tail_lines, minimum=1) + + # Compute SHA over full file contents (metadata-only default) + full_bytes = p.read_bytes() + full_sha = hashlib.sha256(full_bytes).hexdigest() + + # Selection only when explicitly requested via windowing args or request text hints + selection_requested = bool(head_bytes or tail_lines or ( + start_line is not None and line_count is not None) or request) + if selection_requested: + # Mutually exclusive windowing options precedence: + # 1) head_bytes, 2) tail_lines, 3) start_line+line_count, else full text + if head_bytes and head_bytes > 0: + raw = full_bytes[: head_bytes] + text = raw.decode("utf-8", errors="replace") + else: + text = full_bytes.decode("utf-8", errors="replace") + if tail_lines is not None and tail_lines > 0: + lines = text.splitlines() + n = max(0, tail_lines) + text = "\n".join(lines[-n:]) + elif start_line is not None and line_count is not None and line_count >= 0: + lines = text.splitlines() + s = max(0, start_line - 1) + e = min(len(lines), s + line_count) + text = "\n".join(lines[s:e]) + return {"success": True, "data": {"text": text, "metadata": {"sha256": full_sha, "lengthBytes": len(full_bytes)}}} + else: + # Default: metadata only + return {"success": True, "data": {"metadata": {"sha256": full_sha, "lengthBytes": len(full_bytes)}}} + except Exception as e: + return {"success": False, "error": str(e)} + + +@mcp_for_unity_tool(description="Searches a file with a regex pattern and returns line numbers and excerpts.") +async def find_in_file( + ctx: Context, + uri: Annotated[str, "The resource URI to search under Assets/ or file path form supported by read_resource"], + pattern: Annotated[str, "The regex pattern to search for"], + ignore_case: Annotated[bool, "Case-insensitive search"] | None = True, + project_root: Annotated[str, + "The project root directory"] | None = None, + max_results: Annotated[int, + "Cap results to avoid huge payloads"] = 200, +) -> dict[str, Any]: + ctx.info(f"Processing find_in_file: {uri}") + try: + project = _resolve_project_root(project_root) + p = _resolve_safe_path_from_uri(uri, project) + if not p or not p.exists() or not p.is_file(): + return {"success": False, "error": f"Resource not found: {uri}"} + + text = p.read_text(encoding="utf-8") + flags = re.MULTILINE + if ignore_case: + flags |= re.IGNORECASE + rx = re.compile(pattern, flags) + + results = [] + max_results_int = _coerce_int(max_results, default=200, minimum=1) + lines = text.splitlines() + for i, line in enumerate(lines, start=1): + m = rx.search(line) + if m: + start_col = m.start() + 1 # 1-based + end_col = m.end() + 1 # 1-based, end exclusive + results.append({ + "startLine": i, + "startCol": start_col, + "endLine": i, + "endCol": end_col, + }) + if max_results_int and len(results) >= max_results_int: + break + + return {"success": True, "data": {"matches": results, "count": len(results)}} + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py b/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py new file mode 100644 index 00000000..59fbbc61 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py @@ -0,0 +1,966 @@ +import base64 +import hashlib +import re +from typing import Annotated, Any + +from mcp.server.fastmcp import Context + +from registry import mcp_for_unity_tool +from unity_connection import send_command_with_retry + + +def _apply_edits_locally(original_text: str, edits: list[dict[str, Any]]) -> str: + text = original_text + for edit in edits or []: + op = ( + (edit.get("op") + or edit.get("operation") + or edit.get("type") + or edit.get("mode") + or "") + .strip() + .lower() + ) + + if not op: + allowed = "anchor_insert, prepend, append, replace_range, regex_replace" + raise RuntimeError( + f"op is required; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation)." + ) + + if op == "prepend": + prepend_text = edit.get("text", "") + text = (prepend_text if prepend_text.endswith( + "\n") else prepend_text + "\n") + text + elif op == "append": + append_text = edit.get("text", "") + if not text.endswith("\n"): + text += "\n" + text += append_text + if not text.endswith("\n"): + text += "\n" + elif op == "anchor_insert": + anchor = edit.get("anchor", "") + position = (edit.get("position") or "before").lower() + insert_text = edit.get("text", "") + flags = re.MULTILINE | ( + re.IGNORECASE if edit.get("ignore_case") else 0) + + # Find the best match using improved heuristics + match = _find_best_anchor_match( + anchor, text, flags, bool(edit.get("prefer_last", True))) + if not match: + if edit.get("allow_noop", True): + continue + raise RuntimeError(f"anchor not found: {anchor}") + idx = match.start() if position == "before" else match.end() + text = text[:idx] + insert_text + text[idx:] + elif op == "replace_range": + start_line = int(edit.get("startLine", 1)) + start_col = int(edit.get("startCol", 1)) + end_line = int(edit.get("endLine", start_line)) + end_col = int(edit.get("endCol", 1)) + replacement = edit.get("text", "") + lines = text.splitlines(keepends=True) + max_line = len(lines) + 1 # 1-based, exclusive end + if (start_line < 1 or end_line < start_line or end_line > max_line + or start_col < 1 or end_col < 1): + raise RuntimeError("replace_range out of bounds") + + def index_of(line: int, col: int) -> int: + if line <= len(lines): + return sum(len(l) for l in lines[: line - 1]) + (col - 1) + return sum(len(l) for l in lines) + a = index_of(start_line, start_col) + b = index_of(end_line, end_col) + text = text[:a] + replacement + text[b:] + elif op == "regex_replace": + pattern = edit.get("pattern", "") + repl = edit.get("replacement", "") + # Translate $n backrefs (our input) to Python \g + repl_py = re.sub(r"\$(\d+)", r"\\g<\1>", repl) + count = int(edit.get("count", 0)) # 0 = replace all + flags = re.MULTILINE + if edit.get("ignore_case"): + flags |= re.IGNORECASE + text = re.sub(pattern, repl_py, text, count=count, flags=flags) + else: + allowed = "anchor_insert, prepend, append, replace_range, regex_replace" + raise RuntimeError( + f"unknown edit op: {op}; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation).") + return text + + +def _find_best_anchor_match(pattern: str, text: str, flags: int, prefer_last: bool = True): + """ + Find the best anchor match using improved heuristics. + + For patterns like \\s*}\\s*$ that are meant to find class-ending braces, + this function uses heuristics to choose the most semantically appropriate match: + + 1. If prefer_last=True, prefer the last match (common for class-end insertions) + 2. Use indentation levels to distinguish class vs method braces + 3. Consider context to avoid matches inside strings/comments + + Args: + pattern: Regex pattern to search for + text: Text to search in + flags: Regex flags + prefer_last: If True, prefer the last match over the first + + Returns: + Match object of the best match, or None if no match found + """ + + # Find all matches + matches = list(re.finditer(pattern, text, flags)) + if not matches: + return None + + # If only one match, return it + if len(matches) == 1: + return matches[0] + + # For patterns that look like they're trying to match closing braces at end of lines + is_closing_brace_pattern = '}' in pattern and ( + '$' in pattern or pattern.endswith(r'\s*')) + + if is_closing_brace_pattern and prefer_last: + # Use heuristics to find the best closing brace match + return _find_best_closing_brace_match(matches, text) + + # Default behavior: use last match if prefer_last, otherwise first match + return matches[-1] if prefer_last else matches[0] + + +def _find_best_closing_brace_match(matches, text: str): + """ + Find the best closing brace match using C# structure heuristics. + + Enhanced heuristics for scope-aware matching: + 1. Prefer matches with lower indentation (likely class-level) + 2. Prefer matches closer to end of file + 3. Avoid matches that seem to be inside method bodies + 4. For #endregion patterns, ensure class-level context + 5. Validate insertion point is at appropriate scope + + Args: + matches: List of regex match objects + text: The full text being searched + + Returns: + The best match object + """ + if not matches: + return None + + scored_matches = [] + lines = text.splitlines() + + for match in matches: + score = 0 + start_pos = match.start() + + # Find which line this match is on + lines_before = text[:start_pos].count('\n') + line_num = lines_before + + if line_num < len(lines): + line_content = lines[line_num] + + # Calculate indentation level (lower is better for class braces) + indentation = len(line_content) - len(line_content.lstrip()) + + # Prefer lower indentation (class braces are typically less indented than method braces) + # Max 20 points for indentation=0 + score += max(0, 20 - indentation) + + # Prefer matches closer to end of file (class closing braces are typically at the end) + distance_from_end = len(lines) - line_num + # More points for being closer to end + score += max(0, 10 - distance_from_end) + + # Look at surrounding context to avoid method braces + context_start = max(0, line_num - 3) + context_end = min(len(lines), line_num + 2) + context_lines = lines[context_start:context_end] + + # Penalize if this looks like it's inside a method (has method-like patterns above) + for context_line in context_lines: + if re.search(r'\b(void|public|private|protected)\s+\w+\s*\(', context_line): + score -= 5 # Penalty for being near method signatures + + # Bonus if this looks like a class-ending brace (very minimal indentation and near EOF) + if indentation <= 4 and distance_from_end <= 3: + score += 15 # Bonus for likely class-ending brace + + scored_matches.append((score, match)) + + # Return the match with the highest score + scored_matches.sort(key=lambda x: x[0], reverse=True) + best_match = scored_matches[0][1] + + return best_match + + +def _infer_class_name(script_name: str) -> str: + # Default to script name as class name (common Unity pattern) + return (script_name or "").strip() + + +def _extract_code_after(keyword: str, request: str) -> str: + # Deprecated with NL removal; retained as no-op for compatibility + idx = request.lower().find(keyword) + if idx >= 0: + return request[idx + len(keyword):].strip() + return "" +# Removed _is_structurally_balanced - validation now handled by C# side using Unity's compiler services + + +def _normalize_script_locator(name: str, path: str) -> tuple[str, str]: + """Best-effort normalization of script "name" and "path". + + Accepts any of: + - name = "SmartReach", path = "Assets/Scripts/Interaction" + - name = "SmartReach.cs", path = "Assets/Scripts/Interaction" + - name = "Assets/Scripts/Interaction/SmartReach.cs", path = "" + - path = "Assets/Scripts/Interaction/SmartReach.cs" (name empty) + - name or path using uri prefixes: unity://path/..., file://... + - accidental duplicates like "Assets/.../SmartReach.cs/SmartReach.cs" + + Returns (name_without_extension, directory_path_under_Assets). + """ + n = (name or "").strip() + p = (path or "").strip() + + def strip_prefix(s: str) -> str: + if s.startswith("unity://path/"): + return s[len("unity://path/"):] + if s.startswith("file://"): + return s[len("file://"):] + return s + + def collapse_duplicate_tail(s: str) -> str: + # Collapse trailing "/X.cs/X.cs" to "/X.cs" + parts = s.split("/") + if len(parts) >= 2 and parts[-1] == parts[-2]: + parts = parts[:-1] + return "/".join(parts) + + # Prefer a full path if provided in either field + candidate = "" + for v in (n, p): + v2 = strip_prefix(v) + if v2.endswith(".cs") or v2.startswith("Assets/"): + candidate = v2 + break + + if candidate: + candidate = collapse_duplicate_tail(candidate) + # If a directory was passed in path and file in name, join them + if not candidate.endswith(".cs") and n.endswith(".cs"): + v2 = strip_prefix(n) + candidate = (candidate.rstrip("/") + "/" + v2.split("/")[-1]) + if candidate.endswith(".cs"): + parts = candidate.split("/") + file_name = parts[-1] + dir_path = "/".join(parts[:-1]) if len(parts) > 1 else "Assets" + base = file_name[:- + 3] if file_name.lower().endswith(".cs") else file_name + return base, dir_path + + # Fall back: remove extension from name if present and return given path + base_name = n[:-3] if n.lower().endswith(".cs") else n + return base_name, (p or "Assets") + + +def _with_norm(resp: dict[str, Any] | Any, edits: list[dict[str, Any]], routing: str | None = None) -> dict[str, Any] | Any: + if not isinstance(resp, dict): + return resp + data = resp.setdefault("data", {}) + data.setdefault("normalizedEdits", edits) + if routing: + data["routing"] = routing + return resp + + +def _err(code: str, message: str, *, expected: dict[str, Any] | None = None, rewrite: dict[str, Any] | None = None, + normalized: list[dict[str, Any]] | None = None, routing: str | None = None, extra: dict[str, Any] | None = None) -> dict[str, Any]: + payload: dict[str, Any] = {"success": False, + "code": code, "message": message} + data: dict[str, Any] = {} + if expected: + data["expected"] = expected + if rewrite: + data["rewrite_suggestion"] = rewrite + if normalized is not None: + data["normalizedEdits"] = normalized + if routing: + data["routing"] = routing + if extra: + data.update(extra) + if data: + payload["data"] = data + return payload + +# Natural-language parsing removed; clients should send structured edits. + + +@mcp_for_unity_tool(name="script_apply_edits", description=( + """Structured C# edits (methods/classes) with safer boundaries - prefer this over raw text. + Best practices: + - Prefer anchor_* ops for pattern-based insert/replace near stable markers + - Use replace_method/delete_method for whole-method changes (keeps signatures balanced) + - Avoid whole-file regex deletes; validators will guard unbalanced braces + - For tail insertions, prefer anchor/regex_replace on final brace (class closing) + - Pass options.validate='standard' for structural checks; 'relaxed' for interior-only edits + Canonical fields (use these exact keys): + - op: replace_method | insert_method | delete_method | anchor_insert | anchor_delete | anchor_replace + - className: string (defaults to 'name' if omitted on method/class ops) + - methodName: string (required for replace_method, delete_method) + - replacement: string (required for replace_method, insert_method) + - position: start | end | after | before (insert_method only) + - afterMethodName / beforeMethodName: string (required when position='after'/'before') + - anchor: regex string (for anchor_* ops) + - text: string (for anchor_insert/anchor_replace) + Examples: + 1) Replace a method: + { + "name": "SmartReach", + "path": "Assets/Scripts/Interaction", + "edits": [ + { + "op": "replace_method", + "className": "SmartReach", + "methodName": "HasTarget", + "replacement": "public bool HasTarget(){ return currentTarget!=null; }" + } + ], + "options": {"validate": "standard", "refresh": "immediate"} + } + "2) Insert a method after another: + { + "name": "SmartReach", + "path": "Assets/Scripts/Interaction", + "edits": [ + { + "op": "insert_method", + "className": "SmartReach", + "replacement": "public void PrintSeries(){ Debug.Log(seriesName); }", + "position": "after", + "afterMethodName": "GetCurrentTarget" + } + ], + } + ]""" +)) +def script_apply_edits( + ctx: Context, + name: Annotated[str, "Name of the script to edit"], + path: Annotated[str, "Path to the script to edit under Assets/ directory"], + edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script"], + options: Annotated[dict[str, Any], + "Options for the script edit"] | None = None, + script_type: Annotated[str, + "Type of the script to edit"] = "MonoBehaviour", + namespace: Annotated[str, + "Namespace of the script to edit"] | None = None, +) -> dict[str, Any]: + ctx.info(f"Processing script_apply_edits: {name}") + # Normalize locator first so downstream calls target the correct script file. + name, path = _normalize_script_locator(name, path) + # Normalize unsupported or aliased ops to known structured/text paths + + def _unwrap_and_alias(edit: dict[str, Any]) -> dict[str, Any]: + # Unwrap single-key wrappers like {"replace_method": {...}} + for wrapper_key in ( + "replace_method", "insert_method", "delete_method", + "replace_class", "delete_class", + "anchor_insert", "anchor_replace", "anchor_delete", + ): + if wrapper_key in edit and isinstance(edit[wrapper_key], dict): + inner = dict(edit[wrapper_key]) + inner["op"] = wrapper_key + edit = inner + break + + e = dict(edit) + op = (e.get("op") or e.get("operation") or e.get( + "type") or e.get("mode") or "").strip().lower() + if op: + e["op"] = op + + # Common field aliases + if "class_name" in e and "className" not in e: + e["className"] = e.pop("class_name") + if "class" in e and "className" not in e: + e["className"] = e.pop("class") + if "method_name" in e and "methodName" not in e: + e["methodName"] = e.pop("method_name") + # Some clients use a generic 'target' for method name + if "target" in e and "methodName" not in e: + e["methodName"] = e.pop("target") + if "method" in e and "methodName" not in e: + e["methodName"] = e.pop("method") + if "new_content" in e and "replacement" not in e: + e["replacement"] = e.pop("new_content") + if "newMethod" in e and "replacement" not in e: + e["replacement"] = e.pop("newMethod") + if "new_method" in e and "replacement" not in e: + e["replacement"] = e.pop("new_method") + if "content" in e and "replacement" not in e: + e["replacement"] = e.pop("content") + if "after" in e and "afterMethodName" not in e: + e["afterMethodName"] = e.pop("after") + if "after_method" in e and "afterMethodName" not in e: + e["afterMethodName"] = e.pop("after_method") + if "before" in e and "beforeMethodName" not in e: + e["beforeMethodName"] = e.pop("before") + if "before_method" in e and "beforeMethodName" not in e: + e["beforeMethodName"] = e.pop("before_method") + # anchor_method → before/after based on position (default after) + if "anchor_method" in e: + anchor = e.pop("anchor_method") + pos = (e.get("position") or "after").strip().lower() + if pos == "before" and "beforeMethodName" not in e: + e["beforeMethodName"] = anchor + elif "afterMethodName" not in e: + e["afterMethodName"] = anchor + if "anchorText" in e and "anchor" not in e: + e["anchor"] = e.pop("anchorText") + if "pattern" in e and "anchor" not in e and e.get("op") and e["op"].startswith("anchor_"): + e["anchor"] = e.pop("pattern") + if "newText" in e and "text" not in e: + e["text"] = e.pop("newText") + + # CI compatibility (T‑A/T‑E): + # Accept method-anchored anchor_insert and upgrade to insert_method + # Example incoming shape: + # {"op":"anchor_insert","afterMethodName":"GetCurrentTarget","text":"..."} + if ( + e.get("op") == "anchor_insert" + and not e.get("anchor") + and (e.get("afterMethodName") or e.get("beforeMethodName")) + ): + e["op"] = "insert_method" + if "replacement" not in e: + e["replacement"] = e.get("text", "") + + # LSP-like range edit -> replace_range + if "range" in e and isinstance(e["range"], dict): + rng = e.pop("range") + start = rng.get("start", {}) + end = rng.get("end", {}) + # Convert 0-based to 1-based line/col + e["op"] = "replace_range" + e["startLine"] = int(start.get("line", 0)) + 1 + e["startCol"] = int(start.get("character", 0)) + 1 + e["endLine"] = int(end.get("line", 0)) + 1 + e["endCol"] = int(end.get("character", 0)) + 1 + if "newText" in edit and "text" not in e: + e["text"] = edit.get("newText", "") + return e + + normalized_edits: list[dict[str, Any]] = [] + for raw in edits or []: + e = _unwrap_and_alias(raw) + op = (e.get("op") or e.get("operation") or e.get( + "type") or e.get("mode") or "").strip().lower() + + # Default className to script name if missing on structured method/class ops + if op in ("replace_class", "delete_class", "replace_method", "delete_method", "insert_method") and not e.get("className"): + e["className"] = name + + # Map common aliases for text ops + if op in ("text_replace",): + e["op"] = "replace_range" + normalized_edits.append(e) + continue + if op in ("regex_delete",): + e["op"] = "regex_replace" + e.setdefault("text", "") + normalized_edits.append(e) + continue + if op == "regex_replace" and ("replacement" not in e): + if "text" in e: + e["replacement"] = e.get("text", "") + elif "insert" in e or "content" in e: + e["replacement"] = e.get( + "insert") or e.get("content") or "" + if op == "anchor_insert" and not (e.get("text") or e.get("insert") or e.get("content") or e.get("replacement")): + e["op"] = "anchor_delete" + normalized_edits.append(e) + continue + normalized_edits.append(e) + + edits = normalized_edits + normalized_for_echo = edits + + # Validate required fields and produce machine-parsable hints + def error_with_hint(message: str, expected: dict[str, Any], suggestion: dict[str, Any]) -> dict[str, Any]: + return _err("missing_field", message, expected=expected, rewrite=suggestion, normalized=normalized_for_echo) + + for e in edits or []: + op = e.get("op", "") + if op == "replace_method": + if not e.get("methodName"): + return error_with_hint( + "replace_method requires 'methodName'.", + {"op": "replace_method", "required": [ + "className", "methodName", "replacement"]}, + {"edits[0].methodName": "HasTarget"} + ) + if not (e.get("replacement") or e.get("text")): + return error_with_hint( + "replace_method requires 'replacement' (inline or base64).", + {"op": "replace_method", "required": [ + "className", "methodName", "replacement"]}, + {"edits[0].replacement": "public bool X(){ return true; }"} + ) + elif op == "insert_method": + if not (e.get("replacement") or e.get("text")): + return error_with_hint( + "insert_method requires a non-empty 'replacement'.", + {"op": "insert_method", "required": ["className", "replacement"], "position": { + "after_requires": "afterMethodName", "before_requires": "beforeMethodName"}}, + {"edits[0].replacement": "public void PrintSeries(){ Debug.Log(\"1,2,3\"); }"} + ) + pos = (e.get("position") or "").lower() + if pos == "after" and not e.get("afterMethodName"): + return error_with_hint( + "insert_method with position='after' requires 'afterMethodName'.", + {"op": "insert_method", "position": { + "after_requires": "afterMethodName"}}, + {"edits[0].afterMethodName": "GetCurrentTarget"} + ) + if pos == "before" and not e.get("beforeMethodName"): + return error_with_hint( + "insert_method with position='before' requires 'beforeMethodName'.", + {"op": "insert_method", "position": { + "before_requires": "beforeMethodName"}}, + {"edits[0].beforeMethodName": "GetCurrentTarget"} + ) + elif op == "delete_method": + if not e.get("methodName"): + return error_with_hint( + "delete_method requires 'methodName'.", + {"op": "delete_method", "required": [ + "className", "methodName"]}, + {"edits[0].methodName": "PrintSeries"} + ) + elif op in ("anchor_insert", "anchor_replace", "anchor_delete"): + if not e.get("anchor"): + return error_with_hint( + f"{op} requires 'anchor' (regex).", + {"op": op, "required": ["anchor"]}, + {"edits[0].anchor": "(?m)^\\s*public\\s+bool\\s+HasTarget\\s*\\("} + ) + if op in ("anchor_insert", "anchor_replace") and not (e.get("text") or e.get("replacement")): + return error_with_hint( + f"{op} requires 'text'.", + {"op": op, "required": ["anchor", "text"]}, + {"edits[0].text": "/* comment */\n"} + ) + + # Decide routing: structured vs text vs mixed + STRUCT = {"replace_class", "delete_class", "replace_method", "delete_method", + "insert_method", "anchor_delete", "anchor_replace", "anchor_insert"} + TEXT = {"prepend", "append", "replace_range", "regex_replace"} + ops_set = {(e.get("op") or "").lower() for e in edits or []} + all_struct = ops_set.issubset(STRUCT) + all_text = ops_set.issubset(TEXT) + mixed = not (all_struct or all_text) + + # If everything is structured (method/class/anchor ops), forward directly to Unity's structured editor. + if all_struct: + opts2 = dict(options or {}) + # For structured edits, prefer immediate refresh to avoid missed reloads when Editor is unfocused + opts2.setdefault("refresh", "immediate") + params_struct: dict[str, Any] = { + "action": "edit", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": edits, + "options": opts2, + } + resp_struct = send_command_with_retry( + "manage_script", params_struct) + if isinstance(resp_struct, dict) and resp_struct.get("success"): + pass # Optional sentinel reload removed (deprecated) + return _with_norm(resp_struct if isinstance(resp_struct, dict) else {"success": False, "message": str(resp_struct)}, normalized_for_echo, routing="structured") + + # 1) read from Unity + read_resp = send_command_with_retry("manage_script", { + "action": "read", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + }) + if not isinstance(read_resp, dict) or not read_resp.get("success"): + return read_resp if isinstance(read_resp, dict) else {"success": False, "message": str(read_resp)} + + data = read_resp.get("data") or read_resp.get( + "result", {}).get("data") or {} + contents = data.get("contents") + if contents is None and data.get("contentsEncoded") and data.get("encodedContents"): + contents = base64.b64decode( + data["encodedContents"]).decode("utf-8") + if contents is None: + return {"success": False, "message": "No contents returned from Unity read."} + + # Optional preview/dry-run: apply locally and return diff without writing + preview = bool((options or {}).get("preview")) + + # If we have a mixed batch (TEXT + STRUCT), apply text first with precondition, then structured + if mixed: + text_edits = [e for e in edits or [] if ( + e.get("op") or "").lower() in TEXT] + struct_edits = [e for e in edits or [] if ( + e.get("op") or "").lower() in STRUCT] + try: + base_text = contents + + def line_col_from_index(idx: int) -> tuple[int, int]: + line = base_text.count("\n", 0, idx) + 1 + last_nl = base_text.rfind("\n", 0, idx) + col = (idx - (last_nl + 1)) + \ + 1 if last_nl >= 0 else idx + 1 + return line, col + + at_edits: list[dict[str, Any]] = [] + for e in text_edits: + opx = (e.get("op") or e.get("operation") or e.get( + "type") or e.get("mode") or "").strip().lower() + text_field = e.get("text") or e.get("insert") or e.get( + "content") or e.get("replacement") or "" + if opx == "anchor_insert": + anchor = e.get("anchor") or "" + position = (e.get("position") or "after").lower() + flags = re.MULTILINE | ( + re.IGNORECASE if e.get("ignore_case") else 0) + try: + # Use improved anchor matching logic + m = _find_best_anchor_match( + anchor, base_text, flags, prefer_last=True) + except Exception as ex: + return _with_norm(_err("bad_regex", f"Invalid anchor regex: {ex}", normalized=normalized_for_echo, routing="mixed/text-first", extra={"hint": "Escape parentheses/braces or use a simpler anchor."}), normalized_for_echo, routing="mixed/text-first") + if not m: + return _with_norm({"success": False, "code": "anchor_not_found", "message": f"anchor not found: {anchor}"}, normalized_for_echo, routing="mixed/text-first") + idx = m.start() if position == "before" else m.end() + # Normalize insertion to avoid jammed methods + text_field_norm = text_field + if not text_field_norm.startswith("\n"): + text_field_norm = "\n" + text_field_norm + if not text_field_norm.endswith("\n"): + text_field_norm = text_field_norm + "\n" + sl, sc = line_col_from_index(idx) + at_edits.append( + {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": text_field_norm}) + # do not mutate base_text when building atomic spans + elif opx == "replace_range": + if all(k in e for k in ("startLine", "startCol", "endLine", "endCol")): + at_edits.append({ + "startLine": int(e.get("startLine", 1)), + "startCol": int(e.get("startCol", 1)), + "endLine": int(e.get("endLine", 1)), + "endCol": int(e.get("endCol", 1)), + "newText": text_field + }) + else: + return _with_norm(_err("missing_field", "replace_range requires startLine/startCol/endLine/endCol", normalized=normalized_for_echo, routing="mixed/text-first"), normalized_for_echo, routing="mixed/text-first") + elif opx == "regex_replace": + pattern = e.get("pattern") or "" + try: + regex_obj = re.compile(pattern, re.MULTILINE | ( + re.IGNORECASE if e.get("ignore_case") else 0)) + except Exception as ex: + return _with_norm(_err("bad_regex", f"Invalid regex pattern: {ex}", normalized=normalized_for_echo, routing="mixed/text-first", extra={"hint": "Escape special chars or prefer structured delete for methods."}), normalized_for_echo, routing="mixed/text-first") + m = regex_obj.search(base_text) + if not m: + continue + # Expand $1, $2... in replacement using this match + + def _expand_dollars(rep: str, _m=m) -> str: + return re.sub(r"\$(\d+)", lambda g: _m.group(int(g.group(1))) or "", rep) + repl = _expand_dollars(text_field) + sl, sc = line_col_from_index(m.start()) + el, ec = line_col_from_index(m.end()) + at_edits.append( + {"startLine": sl, "startCol": sc, "endLine": el, "endCol": ec, "newText": repl}) + # do not mutate base_text when building atomic spans + elif opx in ("prepend", "append"): + if opx == "prepend": + sl, sc = 1, 1 + at_edits.append( + {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": text_field}) + # prepend can be applied atomically without local mutation + else: + # Insert at true EOF position (handles both \n and \r\n correctly) + eof_idx = len(base_text) + sl, sc = line_col_from_index(eof_idx) + new_text = ("\n" if not base_text.endswith( + "\n") else "") + text_field + at_edits.append( + {"startLine": sl, "startCol": sc, "endLine": sl, "endCol": sc, "newText": new_text}) + # do not mutate base_text when building atomic spans + else: + return _with_norm(_err("unknown_op", f"Unsupported text edit op: {opx}", normalized=normalized_for_echo, routing="mixed/text-first"), normalized_for_echo, routing="mixed/text-first") + + sha = hashlib.sha256(base_text.encode("utf-8")).hexdigest() + if at_edits: + params_text: dict[str, Any] = { + "action": "apply_text_edits", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": at_edits, + "precondition_sha256": sha, + "options": {"refresh": (options or {}).get("refresh", "debounced"), "validate": (options or {}).get("validate", "standard"), "applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential"))} + } + resp_text = send_command_with_retry( + "manage_script", params_text) + if not (isinstance(resp_text, dict) and resp_text.get("success")): + return _with_norm(resp_text if isinstance(resp_text, dict) else {"success": False, "message": str(resp_text)}, normalized_for_echo, routing="mixed/text-first") + # Optional sentinel reload removed (deprecated) + except Exception as e: + return _with_norm({"success": False, "message": f"Text edit conversion failed: {e}"}, normalized_for_echo, routing="mixed/text-first") + + if struct_edits: + opts2 = dict(options or {}) + # Prefer debounced background refresh unless explicitly overridden + opts2.setdefault("refresh", "debounced") + params_struct: dict[str, Any] = { + "action": "edit", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": struct_edits, + "options": opts2 + } + resp_struct = send_command_with_retry( + "manage_script", params_struct) + if isinstance(resp_struct, dict) and resp_struct.get("success"): + pass # Optional sentinel reload removed (deprecated) + return _with_norm(resp_struct if isinstance(resp_struct, dict) else {"success": False, "message": str(resp_struct)}, normalized_for_echo, routing="mixed/text-first") + + return _with_norm({"success": True, "message": "Applied text edits (no structured ops)"}, normalized_for_echo, routing="mixed/text-first") + + # If the edits are text-ops, prefer sending them to Unity's apply_text_edits with precondition + # so header guards and validation run on the C# side. + # Supported conversions: anchor_insert, replace_range, regex_replace (first match only). + text_ops = {(e.get("op") or e.get("operation") or e.get("type") or e.get( + "mode") or "").strip().lower() for e in (edits or [])} + structured_kinds = {"replace_class", "delete_class", + "replace_method", "delete_method", "insert_method", "anchor_insert"} + if not text_ops.issubset(structured_kinds): + # Convert to apply_text_edits payload + try: + base_text = contents + + def line_col_from_index(idx: int) -> tuple[int, int]: + # 1-based line/col against base buffer + line = base_text.count("\n", 0, idx) + 1 + last_nl = base_text.rfind("\n", 0, idx) + col = (idx - (last_nl + 1)) + \ + 1 if last_nl >= 0 else idx + 1 + return line, col + + at_edits: list[dict[str, Any]] = [] + import re as _re + for e in edits or []: + op = (e.get("op") or e.get("operation") or e.get( + "type") or e.get("mode") or "").strip().lower() + # aliasing for text field + text_field = e.get("text") or e.get( + "insert") or e.get("content") or "" + if op == "anchor_insert": + anchor = e.get("anchor") or "" + position = (e.get("position") or "after").lower() + # Use improved anchor matching logic with helpful errors, honoring ignore_case + try: + flags = re.MULTILINE | ( + re.IGNORECASE if e.get("ignore_case") else 0) + m = _find_best_anchor_match( + anchor, base_text, flags, prefer_last=True) + except Exception as ex: + return _with_norm(_err("bad_regex", f"Invalid anchor regex: {ex}", normalized=normalized_for_echo, routing="text", extra={"hint": "Escape parentheses/braces or use a simpler anchor."}), normalized_for_echo, routing="text") + if not m: + return _with_norm({"success": False, "code": "anchor_not_found", "message": f"anchor not found: {anchor}"}, normalized_for_echo, routing="text") + idx = m.start() if position == "before" else m.end() + # Normalize insertion newlines + if text_field and not text_field.startswith("\n"): + text_field = "\n" + text_field + if text_field and not text_field.endswith("\n"): + text_field = text_field + "\n" + sl, sc = line_col_from_index(idx) + at_edits.append({ + "startLine": sl, + "startCol": sc, + "endLine": sl, + "endCol": sc, + "newText": text_field or "" + }) + # Do not mutate base buffer when building an atomic batch + elif op == "replace_range": + # Directly forward if already in line/col form + if "startLine" in e: + at_edits.append({ + "startLine": int(e.get("startLine", 1)), + "startCol": int(e.get("startCol", 1)), + "endLine": int(e.get("endLine", 1)), + "endCol": int(e.get("endCol", 1)), + "newText": text_field + }) + else: + # If only indices provided, skip (we don't support index-based here) + return _with_norm({"success": False, "code": "missing_field", "message": "replace_range requires startLine/startCol/endLine/endCol"}, normalized_for_echo, routing="text") + elif op == "regex_replace": + pattern = e.get("pattern") or "" + repl = text_field + flags = re.MULTILINE | ( + re.IGNORECASE if e.get("ignore_case") else 0) + # Early compile for clearer error messages + try: + regex_obj = re.compile(pattern, flags) + except Exception as ex: + return _with_norm(_err("bad_regex", f"Invalid regex pattern: {ex}", normalized=normalized_for_echo, routing="text", extra={"hint": "Escape special chars or prefer structured delete for methods."}), normalized_for_echo, routing="text") + # Use smart anchor matching for consistent behavior with anchor_insert + m = _find_best_anchor_match( + pattern, base_text, flags, prefer_last=True) + if not m: + continue + # Expand $1, $2... backrefs in replacement using the first match (consistent with mixed-path behavior) + + def _expand_dollars(rep: str, _m=m) -> str: + return re.sub(r"\$(\d+)", lambda g: _m.group(int(g.group(1))) or "", rep) + repl_expanded = _expand_dollars(repl) + # Let C# side handle validation using Unity's built-in compiler services + sl, sc = line_col_from_index(m.start()) + el, ec = line_col_from_index(m.end()) + at_edits.append({ + "startLine": sl, + "startCol": sc, + "endLine": el, + "endCol": ec, + "newText": repl_expanded + }) + # Do not mutate base buffer when building an atomic batch + else: + return _with_norm({"success": False, "code": "unsupported_op", "message": f"Unsupported text edit op for server-side apply_text_edits: {op}"}, normalized_for_echo, routing="text") + + if not at_edits: + return _with_norm({"success": False, "code": "no_spans", "message": "No applicable text edit spans computed (anchor not found or zero-length)."}, normalized_for_echo, routing="text") + + sha = hashlib.sha256(base_text.encode("utf-8")).hexdigest() + params: dict[str, Any] = { + "action": "apply_text_edits", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": at_edits, + "precondition_sha256": sha, + "options": { + "refresh": (options or {}).get("refresh", "debounced"), + "validate": (options or {}).get("validate", "standard"), + "applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential")) + } + } + resp = send_command_with_retry("manage_script", params) + if isinstance(resp, dict) and resp.get("success"): + pass # Optional sentinel reload removed (deprecated) + return _with_norm( + resp if isinstance(resp, dict) else { + "success": False, "message": str(resp)}, + normalized_for_echo, + routing="text" + ) + except Exception as e: + return _with_norm({"success": False, "code": "conversion_failed", "message": f"Edit conversion failed: {e}"}, normalized_for_echo, routing="text") + + # For regex_replace, honor preview consistently: if preview=true, always return diff without writing. + # If confirm=false (default) and preview not requested, return diff and instruct confirm=true to apply. + if "regex_replace" in text_ops and (preview or not (options or {}).get("confirm")): + try: + preview_text = _apply_edits_locally(contents, edits) + import difflib + diff = list(difflib.unified_diff(contents.splitlines( + ), preview_text.splitlines(), fromfile="before", tofile="after", n=2)) + if len(diff) > 800: + diff = diff[:800] + ["... (diff truncated) ..."] + if preview: + return {"success": True, "message": "Preview only (no write)", "data": {"diff": "\n".join(diff), "normalizedEdits": normalized_for_echo}} + return _with_norm({"success": False, "message": "Preview diff; set options.confirm=true to apply.", "data": {"diff": "\n".join(diff)}}, normalized_for_echo, routing="text") + except Exception as e: + return _with_norm({"success": False, "code": "preview_failed", "message": f"Preview failed: {e}"}, normalized_for_echo, routing="text") + # 2) apply edits locally (only if not text-ops) + try: + new_contents = _apply_edits_locally(contents, edits) + except Exception as e: + return {"success": False, "message": f"Edit application failed: {e}"} + + # Short-circuit no-op edits to avoid false "applied" reports downstream + if new_contents == contents: + return _with_norm({ + "success": True, + "message": "No-op: contents unchanged", + "data": {"no_op": True, "evidence": {"reason": "identical_content"}} + }, normalized_for_echo, routing="text") + + if preview: + # Produce a compact unified diff limited to small context + import difflib + a = contents.splitlines() + b = new_contents.splitlines() + diff = list(difflib.unified_diff( + a, b, fromfile="before", tofile="after", n=3)) + # Limit diff size to keep responses small + if len(diff) > 2000: + diff = diff[:2000] + ["... (diff truncated) ..."] + return {"success": True, "message": "Preview only (no write)", "data": {"diff": "\n".join(diff), "normalizedEdits": normalized_for_echo}} + + # 3) update to Unity + # Default refresh/validate for natural usage on text path as well + options = dict(options or {}) + options.setdefault("validate", "standard") + options.setdefault("refresh", "debounced") + + # Compute the SHA of the current file contents for the precondition + old_lines = contents.splitlines(keepends=True) + end_line = len(old_lines) + 1 # 1-based exclusive end + sha = hashlib.sha256(contents.encode("utf-8")).hexdigest() + + # Apply a whole-file text edit rather than the deprecated 'update' action + params = { + "action": "apply_text_edits", + "name": name, + "path": path, + "namespace": namespace, + "scriptType": script_type, + "edits": [ + { + "startLine": 1, + "startCol": 1, + "endLine": end_line, + "endCol": 1, + "newText": new_contents, + } + ], + "precondition_sha256": sha, + "options": options or {"validate": "standard", "refresh": "debounced"}, + } + + write_resp = send_command_with_retry("manage_script", params) + if isinstance(write_resp, dict) and write_resp.get("success"): + pass # Optional sentinel reload removed (deprecated) + return _with_norm( + write_resp if isinstance(write_resp, dict) + else {"success": False, "message": str(write_resp)}, + normalized_for_echo, + routing="text", + ) diff --git a/MCPForUnity/UnityMcpServer~/src/unity_connection.py b/MCPForUnity/UnityMcpServer~/src/unity_connection.py new file mode 100644 index 00000000..f688da34 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/unity_connection.py @@ -0,0 +1,452 @@ +from config import config +import contextlib +from dataclasses import dataclass +import errno +import json +import logging +from pathlib import Path +from port_discovery import PortDiscovery +import random +import socket +import struct +import threading +import time +from typing import Any, Dict + + +# Configure logging using settings from config +logging.basicConfig( + level=getattr(logging, config.log_level), + format=config.log_format +) +logger = logging.getLogger("mcp-for-unity-server") + +# Module-level lock to guard global connection initialization +_connection_lock = threading.Lock() + +# Maximum allowed framed payload size (64 MiB) +FRAMED_MAX = 64 * 1024 * 1024 + + +@dataclass +class UnityConnection: + """Manages the socket connection to the Unity Editor.""" + host: str = config.unity_host + port: int = None # Will be set dynamically + sock: socket.socket = None # Socket for Unity communication + use_framing: bool = False # Negotiated per-connection + + def __post_init__(self): + """Set port from discovery if not explicitly provided""" + if self.port is None: + self.port = PortDiscovery.discover_unity_port() + self._io_lock = threading.Lock() + self._conn_lock = threading.Lock() + + def connect(self) -> bool: + """Establish a connection to the Unity Editor.""" + if self.sock: + return True + with self._conn_lock: + if self.sock: + return True + try: + # Bounded connect to avoid indefinite blocking + connect_timeout = float( + getattr(config, "connect_timeout", getattr(config, "connection_timeout", 1.0))) + self.sock = socket.create_connection( + (self.host, self.port), connect_timeout) + # Disable Nagle's algorithm to reduce small RPC latency + with contextlib.suppress(Exception): + self.sock.setsockopt( + socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + logger.debug(f"Connected to Unity at {self.host}:{self.port}") + + # Strict handshake: require FRAMING=1 + try: + require_framing = getattr(config, "require_framing", True) + timeout = float(getattr(config, "handshake_timeout", 1.0)) + self.sock.settimeout(timeout) + buf = bytearray() + deadline = time.monotonic() + timeout + while time.monotonic() < deadline and len(buf) < 512: + try: + chunk = self.sock.recv(256) + if not chunk: + break + buf.extend(chunk) + if b"\n" in buf: + break + except socket.timeout: + break + text = bytes(buf).decode('ascii', errors='ignore').strip() + + if 'FRAMING=1' in text: + self.use_framing = True + logger.debug( + 'Unity MCP handshake received: FRAMING=1 (strict)') + else: + if require_framing: + # Best-effort plain-text advisory for legacy peers + with contextlib.suppress(Exception): + self.sock.sendall( + b'Unity MCP requires FRAMING=1\n') + raise ConnectionError( + f'Unity MCP requires FRAMING=1, got: {text!r}') + else: + self.use_framing = False + logger.warning( + 'Unity MCP handshake missing FRAMING=1; proceeding in legacy mode by configuration') + finally: + self.sock.settimeout(config.connection_timeout) + return True + except Exception as e: + logger.error(f"Failed to connect to Unity: {str(e)}") + try: + if self.sock: + self.sock.close() + except Exception: + pass + self.sock = None + return False + + def disconnect(self): + """Close the connection to the Unity Editor.""" + if self.sock: + try: + self.sock.close() + except Exception as e: + logger.error(f"Error disconnecting from Unity: {str(e)}") + finally: + self.sock = None + + def _read_exact(self, sock: socket.socket, count: int) -> bytes: + data = bytearray() + while len(data) < count: + chunk = sock.recv(count - len(data)) + if not chunk: + raise ConnectionError( + "Connection closed before reading expected bytes") + data.extend(chunk) + return bytes(data) + + def receive_full_response(self, sock, buffer_size=config.buffer_size) -> bytes: + """Receive a complete response from Unity, handling chunked data.""" + if self.use_framing: + try: + # Consume heartbeats, but do not hang indefinitely if only zero-length frames arrive + heartbeat_count = 0 + deadline = time.monotonic() + getattr(config, 'framed_receive_timeout', 2.0) + while True: + header = self._read_exact(sock, 8) + payload_len = struct.unpack('>Q', header)[0] + if payload_len == 0: + # Heartbeat/no-op frame: consume and continue waiting for a data frame + logger.debug("Received heartbeat frame (length=0)") + heartbeat_count += 1 + if heartbeat_count >= getattr(config, 'max_heartbeat_frames', 16) or time.monotonic() > deadline: + # Treat as empty successful response to match C# server behavior + logger.debug( + "Heartbeat threshold reached; returning empty response") + return b"" + continue + if payload_len > FRAMED_MAX: + raise ValueError( + f"Invalid framed length: {payload_len}") + payload = self._read_exact(sock, payload_len) + logger.debug( + f"Received framed response ({len(payload)} bytes)") + return payload + except socket.timeout as e: + logger.warning("Socket timeout during framed receive") + raise TimeoutError("Timeout receiving Unity response") from e + except Exception as e: + logger.error(f"Error during framed receive: {str(e)}") + raise + + chunks = [] + # Respect the socket's currently configured timeout + try: + while True: + chunk = sock.recv(buffer_size) + if not chunk: + if not chunks: + raise Exception( + "Connection closed before receiving data") + break + chunks.append(chunk) + + # Process the data received so far + data = b''.join(chunks) + decoded_data = data.decode('utf-8') + + # Check if we've received a complete response + try: + # Special case for ping-pong + if decoded_data.strip().startswith('{"status":"success","result":{"message":"pong"'): + logger.debug("Received ping response") + return data + + # Handle escaped quotes in the content + if '"content":' in decoded_data: + # Find the content field and its value + content_start = decoded_data.find('"content":') + 9 + content_end = decoded_data.rfind('"', content_start) + if content_end > content_start: + # Replace escaped quotes in content with regular quotes + content = decoded_data[content_start:content_end] + content = content.replace('\\"', '"') + decoded_data = decoded_data[:content_start] + \ + content + decoded_data[content_end:] + + # Validate JSON format + json.loads(decoded_data) + + # If we get here, we have valid JSON + logger.info( + f"Received complete response ({len(data)} bytes)") + return data + except json.JSONDecodeError: + # We haven't received a complete valid JSON response yet + continue + except Exception as e: + logger.warning( + f"Error processing response chunk: {str(e)}") + # Continue reading more chunks as this might not be the complete response + continue + except socket.timeout: + logger.warning("Socket timeout during receive") + raise Exception("Timeout receiving Unity response") + except Exception as e: + logger.error(f"Error during receive: {str(e)}") + raise + + def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]: + """Send a command with retry/backoff and port rediscovery. Pings only when requested.""" + # Defensive guard: catch empty/placeholder invocations early + if not command_type: + raise ValueError("MCP call missing command_type") + if params is None: + # Return a fast, structured error that clients can display without hanging + return {"success": False, "error": "MCP call received with no parameters (client placeholder?)"} + attempts = max(config.max_retries, 5) + base_backoff = max(0.5, config.retry_delay) + + def read_status_file() -> dict | None: + try: + status_files = sorted(Path.home().joinpath( + '.unity-mcp').glob('unity-mcp-status-*.json'), key=lambda p: p.stat().st_mtime, reverse=True) + if not status_files: + return None + latest = status_files[0] + with latest.open('r') as f: + return json.load(f) + except Exception: + return None + + last_short_timeout = None + + # Preflight: if Unity reports reloading, return a structured hint so clients can retry politely + try: + status = read_status_file() + if status and (status.get('reloading') or status.get('reason') == 'reloading'): + return { + "success": False, + "state": "reloading", + "retry_after_ms": int(config.reload_retry_ms), + "error": "Unity domain reload in progress", + "message": "Unity is reloading scripts; please retry shortly" + } + except Exception: + pass + + for attempt in range(attempts + 1): + try: + # Ensure connected (handshake occurs within connect()) + if not self.sock and not self.connect(): + raise Exception("Could not connect to Unity") + + # Build payload + if command_type == 'ping': + payload = b'ping' + else: + command = {"type": command_type, "params": params or {}} + payload = json.dumps( + command, ensure_ascii=False).encode('utf-8') + + # Send/receive are serialized to protect the shared socket + with self._io_lock: + mode = 'framed' if self.use_framing else 'legacy' + with contextlib.suppress(Exception): + logger.debug( + "send %d bytes; mode=%s; head=%s", + len(payload), + mode, + (payload[:32]).decode('utf-8', 'ignore'), + ) + if self.use_framing: + header = struct.pack('>Q', len(payload)) + self.sock.sendall(header) + self.sock.sendall(payload) + else: + self.sock.sendall(payload) + + # During retry bursts use a short receive timeout and ensure restoration + restore_timeout = None + if attempt > 0 and last_short_timeout is None: + restore_timeout = self.sock.gettimeout() + self.sock.settimeout(1.0) + try: + response_data = self.receive_full_response(self.sock) + with contextlib.suppress(Exception): + logger.debug("recv %d bytes; mode=%s", + len(response_data), mode) + finally: + if restore_timeout is not None: + self.sock.settimeout(restore_timeout) + last_short_timeout = None + + # Parse + if command_type == 'ping': + resp = json.loads(response_data.decode('utf-8')) + if resp.get('status') == 'success' and resp.get('result', {}).get('message') == 'pong': + return {"message": "pong"} + raise Exception("Ping unsuccessful") + + resp = json.loads(response_data.decode('utf-8')) + if resp.get('status') == 'error': + err = resp.get('error') or resp.get( + 'message', 'Unknown Unity error') + raise Exception(err) + return resp.get('result', {}) + except Exception as e: + logger.warning( + f"Unity communication attempt {attempt+1} failed: {e}") + try: + if self.sock: + self.sock.close() + finally: + self.sock = None + + # Re-discover port each time + try: + new_port = PortDiscovery.discover_unity_port() + if new_port != self.port: + logger.info( + f"Unity port changed {self.port} -> {new_port}") + self.port = new_port + except Exception as de: + logger.debug(f"Port discovery failed: {de}") + + if attempt < attempts: + # Heartbeat-aware, jittered backoff + status = read_status_file() + # Base exponential backoff + backoff = base_backoff * (2 ** attempt) + # Decorrelated jitter multiplier + jitter = random.uniform(0.1, 0.3) + + # Fast‑retry for transient socket failures + fast_error = isinstance( + e, (ConnectionRefusedError, ConnectionResetError, TimeoutError)) + if not fast_error: + try: + err_no = getattr(e, 'errno', None) + fast_error = err_no in ( + errno.ECONNREFUSED, errno.ECONNRESET, errno.ETIMEDOUT) + except Exception: + pass + + # Cap backoff depending on state + if status and status.get('reloading'): + cap = 0.8 + elif fast_error: + cap = 0.25 + else: + cap = 3.0 + + sleep_s = min(cap, jitter * (2 ** attempt)) + time.sleep(sleep_s) + continue + raise + + +# Global Unity connection +_unity_connection = None + + +def get_unity_connection() -> UnityConnection: + """Retrieve or establish a persistent Unity connection. + + Note: Do NOT ping on every retrieval to avoid connection storms. Rely on + send_command() exceptions to detect broken sockets and reconnect there. + """ + global _unity_connection + if _unity_connection is not None: + return _unity_connection + + # Double-checked locking to avoid concurrent socket creation + with _connection_lock: + if _unity_connection is not None: + return _unity_connection + logger.info("Creating new Unity connection") + _unity_connection = UnityConnection() + if not _unity_connection.connect(): + _unity_connection = None + raise ConnectionError( + "Could not connect to Unity. Ensure the Unity Editor and MCP Bridge are running.") + logger.info("Connected to Unity on startup") + return _unity_connection + + +# ----------------------------- +# Centralized retry helpers +# ----------------------------- + +def _is_reloading_response(resp: dict) -> bool: + """Return True if the Unity response indicates the editor is reloading.""" + if not isinstance(resp, dict): + return False + if resp.get("state") == "reloading": + return True + message_text = (resp.get("message") or resp.get("error") or "").lower() + return "reload" in message_text + + +def send_command_with_retry(command_type: str, params: Dict[str, Any], *, max_retries: int | None = None, retry_ms: int | None = None) -> Dict[str, Any]: + """Send a command via the shared connection, waiting politely through Unity reloads. + + Uses config.reload_retry_ms and config.reload_max_retries by default. Preserves the + structured failure if retries are exhausted. + """ + conn = get_unity_connection() + if max_retries is None: + max_retries = getattr(config, "reload_max_retries", 40) + if retry_ms is None: + retry_ms = getattr(config, "reload_retry_ms", 250) + + response = conn.send_command(command_type, params) + retries = 0 + while _is_reloading_response(response) and retries < max_retries: + delay_ms = int(response.get("retry_after_ms", retry_ms) + ) if isinstance(response, dict) else retry_ms + time.sleep(max(0.0, delay_ms / 1000.0)) + retries += 1 + response = conn.send_command(command_type, params) + return response + + +async def async_send_command_with_retry(command_type: str, params: Dict[str, Any], *, loop=None, max_retries: int | None = None, retry_ms: int | None = None) -> Dict[str, Any]: + """Async wrapper that runs the blocking retry helper in a thread pool.""" + try: + import asyncio # local import to avoid mandatory asyncio dependency for sync callers + if loop is None: + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + None, + lambda: send_command_with_retry( + command_type, params, max_retries=max_retries, retry_ms=retry_ms), + ) + except Exception as e: + # Return a structured error dict for consistency with other responses + return {"success": False, "error": f"Python async retry helper failed: {str(e)}"} diff --git a/MCPForUnity/UnityMcpServer~/src/uv.lock b/MCPForUnity/UnityMcpServer~/src/uv.lock new file mode 100644 index 00000000..f5cac0f5 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/uv.lock @@ -0,0 +1,400 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mcp" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/cc/5c5bb19f1a0f8f89a95e25cb608b0b07009e81fd4b031e519335404e1422/mcp-1.4.1.tar.gz", hash = "sha256:b9655d2de6313f9d55a7d1df62b3c3fe27a530100cc85bf23729145b0dba4c7a", size = 154942 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/0e/885f156ade60108e67bf044fada5269da68e29d758a10b0c513f4d85dd76/mcp-1.4.1-py3-none-any.whl", hash = "sha256:a7716b1ec1c054e76f49806f7d96113b99fc1166fc9244c2c6f19867cb75b593", size = 72448 }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mcpforunityserver" +version = "3.1.0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "mcp", extra = ["cli"] }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27.2" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.4.1" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, + { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, + { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, + { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, + { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, + { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, + { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, + { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, + { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, + { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, + { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, + { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, + { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, + { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, + { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, + { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, + { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, + { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, + { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sse-starlette" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, +] + +[[package]] +name = "starlette" +version = "0.46.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, +] + +[[package]] +name = "typer" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, +] diff --git a/MCPForUnity/package.json b/MCPForUnity/package.json new file mode 100644 index 00000000..b239aca5 --- /dev/null +++ b/MCPForUnity/package.json @@ -0,0 +1,26 @@ +{ + "name": "com.coplaydev.unity-mcp", + "version": "4.1.1", + "displayName": "MCP for Unity", + "description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4", + "unity": "2021.3", + "documentationUrl": "https://github.com/CoplayDev/unity-mcp", + "licensesUrl": "https://github.com/CoplayDev/unity-mcp/blob/main/LICENSE", + "dependencies": { + "com.unity.nuget.newtonsoft-json": "3.0.2" + }, + "keywords": [ + "unity", + "ai", + "llm", + "mcp", + "model-context-protocol", + "mcp-server", + "mcp-client" + ], + "author": { + "name": "Coplay", + "email": "support@coplay.dev", + "url": "https://coplay.dev" + } +} diff --git a/MCPForUnity/package.json.meta b/MCPForUnity/package.json.meta new file mode 100644 index 00000000..5a96937c --- /dev/null +++ b/MCPForUnity/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a2f7ae0675bf4fb478a0a1df7a3f6c64 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README-zh.md b/README-zh.md index 62c6ae01..f047bef2 100644 --- a/README-zh.md +++ b/README-zh.md @@ -114,7 +114,7 @@ MCP for Unity 使用两个组件连接您的工具: 3. 点击 `+` -> `Add package from git URL...`。 4. 输入: ``` - https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge + https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity ``` 5. 点击 `Add`。 6. MCP 服务器在首次运行时或通过自动设置由包自动安装。如果失败,请使用手动配置(如下)。 diff --git a/README.md b/README.md index 074b0dc6..2093c680 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ MCP for Unity connects your tools using two components: 3. Click `+` -> `Add package from git URL...`. 4. Enter: ``` - https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge + https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity ``` 5. Click `Add`. 6. The MCP server is installed automatically by the package on first run or via Auto-Setup. If that fails, use Manual Configuration (below). diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs index aed1c964..ab204ef0 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs @@ -10,6 +10,13 @@ namespace MCPForUnityTests.Editor.Tools { public class CommandRegistryTests { + [OneTimeSetUp] + public void OneTimeSetUp() + { + // Ensure CommandRegistry is initialized before tests run + CommandRegistry.Initialize(); + } + [Test] public void GetHandler_ThrowsException_ForUnknownCommand() { diff --git a/TestProjects/UnityMCPTests/Packages/manifest.json b/TestProjects/UnityMCPTests/Packages/manifest.json index d378a0a8..b8bbe318 100644 --- a/TestProjects/UnityMCPTests/Packages/manifest.json +++ b/TestProjects/UnityMCPTests/Packages/manifest.json @@ -1,6 +1,6 @@ { "dependencies": { - "com.coplaydev.unity-mcp": "file:../../../UnityMcpBridge", + "com.coplaydev.unity-mcp": "file:../../../MCPForUnity", "com.unity.collab-proxy": "2.5.2", "com.unity.feature.development": "1.0.1", "com.unity.ide.rider": "3.0.31", diff --git a/deploy-dev.bat b/deploy-dev.bat index ca9abea4..60a398bd 100644 --- a/deploy-dev.bat +++ b/deploy-dev.bat @@ -8,8 +8,8 @@ echo. :: Configuration set "SCRIPT_DIR=%~dp0" -set "BRIDGE_SOURCE=%SCRIPT_DIR%UnityMcpBridge" -set "SERVER_SOURCE=%SCRIPT_DIR%UnityMcpBridge\UnityMcpServer~\src" +set "BRIDGE_SOURCE=%SCRIPT_DIR%MCPForUnity" +set "SERVER_SOURCE=%SCRIPT_DIR%MCPForUnity\UnityMcpServer~\src" set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup" set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src" diff --git a/docs/CUSTOM_TOOLS.md b/docs/CUSTOM_TOOLS.md index 1b988e1c..26614a2b 100644 --- a/docs/CUSTOM_TOOLS.md +++ b/docs/CUSTOM_TOOLS.md @@ -11,7 +11,7 @@ Be sure to review the developer README first: ### Creating a Custom Tool -1. **Create a new Python file** in `UnityMcpBridge/UnityMcpServer~/src/tools/` (or any location that gets imported) +1. **Create a new Python file** in `MCPForUnity/UnityMcpServer~/src/tools/` (or any location that gets imported) 2. **Use the `@mcp_for_unity_tool` decorator**: diff --git a/docs/README-DEV-zh.md b/docs/README-DEV-zh.md index 1513cf95..f299a1b5 100644 --- a/docs/README-DEV-zh.md +++ b/docs/README-DEV-zh.md @@ -31,7 +31,7 @@ python mcp_source.py [--manifest /path/to/manifest.json] [--repo /path/to/unity- **选项:** - **1** 上游主分支 (CoplayDev/unity-mcp) - **2** 远程当前分支 (origin + branch) -- **3** 本地工作区 (file: UnityMcpBridge) +- **3** 本地工作区 (file: MCPForUnity) 切换后,打开包管理器并刷新以重新解析包。 diff --git a/docs/README-DEV.md b/docs/README-DEV.md index 3bc63566..d1ef4e46 100644 --- a/docs/README-DEV.md +++ b/docs/README-DEV.md @@ -31,7 +31,7 @@ python mcp_source.py [--manifest /path/to/manifest.json] [--repo /path/to/unity- **Options:** - **1** Upstream main (CoplayDev/unity-mcp) - **2** Remote current branch (origin + branch) -- **3** Local workspace (file: UnityMcpBridge) +- **3** Local workspace (file: MCPForUnity) After switching, open Package Manager and Refresh to re-resolve packages. diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md index f5105d6c..f424ed46 100644 --- a/docs/TELEMETRY.md +++ b/docs/TELEMETRY.md @@ -107,7 +107,7 @@ Files created: ### Testing Telemetry ```bash -cd UnityMcpBridge/UnityMcpServer~/src +cd MCPForUnity/UnityMcpServer~/src python test_telemetry.py ``` diff --git a/mcp_source.py b/mcp_source.py index 203dbe75..4b1390ea 100755 --- a/mcp_source.py +++ b/mcp_source.py @@ -9,22 +9,20 @@ Choices: 1) Upstream main (CoplayDev/unity-mcp) 2) Your remote current branch (derived from `origin` and current branch) - 3) Local repo workspace (file: URL to UnityMcpBridge in your checkout) + 3) Local repo workspace (file: URL to MCPForUnity in your checkout) """ from __future__ import annotations import argparse import json -import os import pathlib -import re import subprocess import sys from typing import Optional PKG_NAME = "com.coplaydev.unity-mcp" -BRIDGE_SUBPATH = "UnityMcpBridge" +BRIDGE_SUBPATH = "MCPForUnity" def run_git(repo: pathlib.Path, *args: str) -> str: @@ -94,7 +92,7 @@ def write_json(path: pathlib.Path, data: dict) -> None: def build_options(repo_root: pathlib.Path, branch: str, origin_https: str): - upstream = "git+https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge" + upstream = "git+https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity" # Ensure origin is https origin = origin_https # If origin is a local file path or non-https, try to coerce to https github if possible diff --git a/prune_tool_results.py b/prune_tool_results.py index b5a53d30..a3c5d7a4 100755 --- a/prune_tool_results.py +++ b/prune_tool_results.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -import sys, json, re +import sys, json def summarize(txt): try: diff --git a/restore-dev.bat b/restore-dev.bat index 51ca2286..6f68be0b 100644 --- a/restore-dev.bat +++ b/restore-dev.bat @@ -5,7 +5,7 @@ echo =============================================== echo MCP for Unity Development Restore Script echo =============================================== echo. -echo Note: The Python server is bundled under UnityMcpBridge\UnityMcpServer~ in the package. +echo Note: The Python server is bundled under MCPForUnity\UnityMcpServer~ in the package. echo This script restores your installed server path from backups, not the repo copy. echo. diff --git a/tests/test_edit_normalization_and_noop.py b/tests/test_edit_normalization_and_noop.py index 86f60afa..bf4e9b79 100644 --- a/tests/test_edit_normalization_and_noop.py +++ b/tests/test_edit_normalization_and_noop.py @@ -5,7 +5,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # stub mcp.server.fastmcp diff --git a/tests/test_edit_strict_and_warnings.py b/tests/test_edit_strict_and_warnings.py index 8ae3bdb6..64b4843c 100644 --- a/tests/test_edit_strict_and_warnings.py +++ b/tests/test_edit_strict_and_warnings.py @@ -5,7 +5,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # stub mcp.server.fastmcp diff --git a/tests/test_find_in_file_minimal.py b/tests/test_find_in_file_minimal.py index a0ebd3b3..92216f60 100644 --- a/tests/test_find_in_file_minimal.py +++ b/tests/test_find_in_file_minimal.py @@ -7,7 +7,7 @@ import pytest ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) diff --git a/tests/test_get_sha.py b/tests/test_get_sha.py index f0a0d7fa..65b59b01 100644 --- a/tests/test_get_sha.py +++ b/tests/test_get_sha.py @@ -5,7 +5,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # stub mcp.server.fastmcp to satisfy imports without full dependency diff --git a/tests/test_improved_anchor_matching.py b/tests/test_improved_anchor_matching.py index b3047c92..cf3ced1f 100644 --- a/tests/test_improved_anchor_matching.py +++ b/tests/test_improved_anchor_matching.py @@ -9,7 +9,7 @@ # add server src to path and load modules ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # stub mcp.server.fastmcp diff --git a/tests/test_logging_stdout.py b/tests/test_logging_stdout.py index 9f5f8495..c69e1af3 100644 --- a/tests/test_logging_stdout.py +++ b/tests/test_logging_stdout.py @@ -7,7 +7,7 @@ # locate server src dynamically to avoid hardcoded layout assumptions ROOT = Path(__file__).resolve().parents[1] candidates = [ - ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src", + ROOT / "MCPForUnity" / "UnityMcpServer~" / "src", ROOT / "UnityMcpServer~" / "src", ] SRC = next((p for p in candidates if p.exists()), None) diff --git a/tests/test_manage_script_uri.py b/tests/test_manage_script_uri.py index d2515922..3d693baa 100644 --- a/tests/test_manage_script_uri.py +++ b/tests/test_manage_script_uri.py @@ -9,7 +9,7 @@ # Locate server src dynamically to avoid hardcoded layout assumptions (same as other tests) ROOT = Path(__file__).resolve().parents[1] candidates = [ - ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src", + ROOT / "MCPForUnity" / "UnityMcpServer~" / "src", ROOT / "UnityMcpServer~" / "src", ] SRC = next((p for p in candidates if p.exists()), None) diff --git a/tests/test_read_console_truncate.py b/tests/test_read_console_truncate.py index 6392d3bc..dab8f904 100644 --- a/tests/test_read_console_truncate.py +++ b/tests/test_read_console_truncate.py @@ -4,7 +4,7 @@ import types ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # stub mcp.server.fastmcp diff --git a/tests/test_read_resource_minimal.py b/tests/test_read_resource_minimal.py index 7f68a919..10ecf33f 100644 --- a/tests/test_read_resource_minimal.py +++ b/tests/test_read_resource_minimal.py @@ -6,7 +6,7 @@ import pytest ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # Stub mcp.server.fastmcp to satisfy imports without full package diff --git a/tests/test_resources_api.py b/tests/test_resources_api.py index 209fa4fd..94243e34 100644 --- a/tests/test_resources_api.py +++ b/tests/test_resources_api.py @@ -10,7 +10,7 @@ # locate server src dynamically to avoid hardcoded layout assumptions ROOT = Path(__file__).resolve().parents[1] candidates = [ - ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src", + ROOT / "MCPForUnity" / "UnityMcpServer~" / "src", ROOT / "UnityMcpServer~" / "src", ] SRC = next((p for p in candidates if p.exists()), None) diff --git a/tests/test_script_tools.py b/tests/test_script_tools.py index aa14503b..f6e3c8a1 100644 --- a/tests/test_script_tools.py +++ b/tests/test_script_tools.py @@ -7,7 +7,7 @@ # add server src to path and load modules without triggering package imports ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # stub mcp.server.fastmcp to satisfy imports without full dependency diff --git a/tests/test_telemetry_endpoint_validation.py b/tests/test_telemetry_endpoint_validation.py index 16de719c..c896860d 100644 --- a/tests/test_telemetry_endpoint_validation.py +++ b/tests/test_telemetry_endpoint_validation.py @@ -8,7 +8,7 @@ def test_endpoint_rejects_non_http(tmp_path, monkeypatch): monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "file:///etc/passwd") telemetry = importlib.import_module( - "UnityMcpBridge.UnityMcpServer~.src.telemetry") + "MCPForUnity.UnityMcpServer~.src.telemetry") importlib.reload(telemetry) tc = telemetry.TelemetryCollector() @@ -24,12 +24,12 @@ def test_config_preferred_then_env_override(tmp_path, monkeypatch): # Patch config.telemetry_endpoint via import mocking import importlib cfg_mod = importlib.import_module( - "UnityMcpBridge.UnityMcpServer~.src.config") + "MCPForUnity.UnityMcpServer~.src.config") old_endpoint = cfg_mod.config.telemetry_endpoint cfg_mod.config.telemetry_endpoint = "https://example.com/telemetry" try: telemetry = importlib.import_module( - "UnityMcpBridge.UnityMcpServer~.src.telemetry") + "MCPForUnity.UnityMcpServer~.src.telemetry") importlib.reload(telemetry) tc = telemetry.TelemetryCollector() assert tc.config.endpoint == "https://example.com/telemetry" @@ -48,7 +48,7 @@ def test_uuid_preserved_on_malformed_milestones(tmp_path, monkeypatch): monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) telemetry = importlib.import_module( - "UnityMcpBridge.UnityMcpServer~.src.telemetry") + "MCPForUnity.UnityMcpServer~.src.telemetry") importlib.reload(telemetry) tc1 = telemetry.TelemetryCollector() diff --git a/tests/test_telemetry_queue_worker.py b/tests/test_telemetry_queue_worker.py index d323d094..a0b54529 100644 --- a/tests/test_telemetry_queue_worker.py +++ b/tests/test_telemetry_queue_worker.py @@ -8,7 +8,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # Stub mcp.server.fastmcp to satisfy imports without the full dependency diff --git a/tests/test_telemetry_subaction.py b/tests/test_telemetry_subaction.py index b74b5ea1..18ece981 100644 --- a/tests/test_telemetry_subaction.py +++ b/tests/test_telemetry_subaction.py @@ -4,7 +4,7 @@ def _get_decorator_module(): # Import the telemetry_decorator module from the Unity MCP server src mod = importlib.import_module( - "UnityMcpBridge.UnityMcpServer~.src.telemetry_decorator") + "MCPForUnity.UnityMcpServer~.src.telemetry_decorator") return mod diff --git a/tests/test_transport_framing.py b/tests/test_transport_framing.py index 882f912c..765a05a4 100644 --- a/tests/test_transport_framing.py +++ b/tests/test_transport_framing.py @@ -13,7 +13,7 @@ # locate server src dynamically to avoid hardcoded layout assumptions ROOT = Path(__file__).resolve().parents[1] candidates = [ - ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src", + ROOT / "MCPForUnity" / "UnityMcpServer~" / "src", ROOT / "UnityMcpServer~" / "src", ] SRC = next((p for p in candidates if p.exists()), None) diff --git a/tests/test_validate_script_summary.py b/tests/test_validate_script_summary.py index f9638128..971b52b7 100644 --- a/tests/test_validate_script_summary.py +++ b/tests/test_validate_script_summary.py @@ -4,7 +4,7 @@ import types ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "UnityMcpBridge" / "UnityMcpServer~" / "src" +SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" sys.path.insert(0, str(SRC)) # stub mcp.server.fastmcp similar to test_get_sha From 9d9652eb4dd469b93d3aaedd4447153e9f246dbd Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 3 Oct 2025 20:56:02 -0400 Subject: [PATCH 6/7] docs: add v5 migration guide with installation screenshots and steps --- .../Editor/Windows/MCPForUnityEditorWindow.cs | 1 - docs/screenshots/v5_01_uninstall.png | Bin 0 -> 542110 bytes docs/screenshots/v5_02_install.png | Bin 0 -> 343474 bytes docs/screenshots/v5_03_open_mcp_window.png | Bin 0 -> 247385 bytes docs/screenshots/v5_04_rebuild_mcp_server.png | Bin 0 -> 417026 bytes docs/screenshots/v5_05_rebuild_success.png | Bin 0 -> 195405 bytes docs/v5_MIGRATION.md | 54 ++++++++++++++++++ 7 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 docs/screenshots/v5_01_uninstall.png create mode 100644 docs/screenshots/v5_02_install.png create mode 100644 docs/screenshots/v5_03_open_mcp_window.png create mode 100644 docs/screenshots/v5_04_rebuild_mcp_server.png create mode 100644 docs/screenshots/v5_05_rebuild_success.png create mode 100644 docs/v5_MIGRATION.md diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs index 98a5295e..fdbfcbb5 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -45,7 +45,6 @@ public class MCPForUnityEditorWindow : EditorWindow // UI state private int selectedClientIndex = 0; - [MenuItem("Window/MCP For Unity")] public static void ShowWindow() { GetWindow("MCP For Unity"); diff --git a/docs/screenshots/v5_01_uninstall.png b/docs/screenshots/v5_01_uninstall.png new file mode 100644 index 0000000000000000000000000000000000000000..6e726781a9efac98a033b8969f07b9d5c8e0b2f8 GIT binary patch literal 542110 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 z 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

y>0wc#jo^KGVGqNWb5nsVRso8 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%0DLMo*7uESa=rpL%I%Rt55Aa!HK#_%PBEHe*gy# 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 EFZAm9Gynhq literal 0 HcmV?d00001 diff --git a/docs/screenshots/v5_02_install.png b/docs/screenshots/v5_02_install.png new file mode 100644 index 0000000000000000000000000000000000000000..8cf0a9fdd6abf8f9d59c90ae143d962328e23fba GIT binary patch literal 343474 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 #S!D!vGT}W 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ⅅdMLc$vN58aqUK` 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/v5_03_open_mcp_window.png b/docs/screenshots/v5_03_open_mcp_window.png new file mode 100644 index 0000000000000000000000000000000000000000..1ea0d6f56bfe252084db108820a3bf4bfc8c4419 GIT binary patch literal 247385 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/v5_04_rebuild_mcp_server.png b/docs/screenshots/v5_04_rebuild_mcp_server.png new file mode 100644 index 0000000000000000000000000000000000000000..260f24ef0ef3c34ef94e99d83ca2c7ef89db9845 GIT binary patch literal 417026 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̐T7BRzS&;c8HkX^^nGE!>l2V-6FSJ~Bsr3v4 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


qg(6 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 Ch^cA- literal 0 HcmV?d00001 diff --git a/docs/screenshots/v5_05_rebuild_success.png b/docs/screenshots/v5_05_rebuild_success.png new file mode 100644 index 0000000000000000000000000000000000000000..b9293ae9ff4cce0168d201a1db981b83713df1f1 GIT binary patch literal 195405 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�^v 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�nJ{D+?h!CAYSKp;oH_jl_%F*rR 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%0AP=#IG_e6F)$dSf0;34ibL04I8U@U3 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 literal 0 HcmV?d00001 diff --git a/docs/v5_MIGRATION.md b/docs/v5_MIGRATION.md new file mode 100644 index 00000000..91aa87e9 --- /dev/null +++ b/docs/v5_MIGRATION.md @@ -0,0 +1,54 @@ +# MCP for Unity v5 Migration Guide + +This guide will help you migrate from the legacy UnityMcpBridge installation to the new MCPForUnity package structure in version 5. + +## Overview + +Version 5 introduces a new package structure. The package is now installed from the `MCPForUnity` folder instead of the legacy `UnityMcpBridge` folder. + +## Migration Steps + +### Step 1: Uninstall the Current Package + +1. Open the Unity Package Manager (**Window > Package Manager**) +2. Select **Packages: In Project** from the dropdown +3. Find **MCP for Unity** in the list +4. Click the **Remove** button to uninstall the legacy package + +![Uninstalling the legacy package](screenshots/v5_01_uninstall.png) + +### Step 2: Install from the New Path + +1. In the Package Manager, click the **+** button in the top-left corner +2. Select **Add package from disk...** +3. Navigate to the `MCPForUnity` folder (NOT the old `UnityMcpBridge` folder) +4. Select the `package.json` file inside the `MCPForUnity` folder +5. Click **Open** to install the package + +![Installing from the new MCPForUnity path](screenshots/v5_02_install.png) + +### Step 3: Rebuild MCP Server + +After installing the new package, you need to rebuild the MCP server: + +1. In Unity, go to **Window > MCP for Unity > Open MCP Window** +![Opening the MCP window](screenshots/v5_03_open_mcp_window.png) +2. Click the **Rebuild MCP Server** button +![Rebuilding the MCP server](screenshots/v5_04_rebuild_mcp_server.png) +3. You should see a success message confirming the rebuild +![Rebuild success](screenshots/v5_05_rebuild_success.png) + +## Verification + +After completing these steps, verify the migration was successful: + +- Check that the package appears in the Package Manager as **MCP for Unity** +- Confirm the package location shows the new `MCPForUnity` path +- Test basic MCP functionality to ensure everything works correctly + +## Troubleshooting + +- Check the Unity Console for specific error messages +- Ensure Python dependencies are properly installed +- Try pressing the rebuild button again +- Try restarting Unity and repeating the installation steps From ba1e488f3298557238b1e638afefaaaebccfdecf Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sat, 4 Oct 2025 01:01:06 +0000 Subject: [PATCH 7/7] chore: bump version to 5.0.0 --- MCPForUnity/UnityMcpServer~/src/pyproject.toml | 2 +- MCPForUnity/UnityMcpServer~/src/server_version.txt | 2 +- MCPForUnity/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/pyproject.toml b/MCPForUnity/UnityMcpServer~/src/pyproject.toml index d6d6d00a..0d624bf8 100644 --- a/MCPForUnity/UnityMcpServer~/src/pyproject.toml +++ b/MCPForUnity/UnityMcpServer~/src/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "MCPForUnityServer" -version = "4.1.1" +version = "5.0.0" description = "MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)." readme = "README.md" requires-python = ">=3.10" diff --git a/MCPForUnity/UnityMcpServer~/src/server_version.txt b/MCPForUnity/UnityMcpServer~/src/server_version.txt index 627a3f43..0062ac97 100644 --- a/MCPForUnity/UnityMcpServer~/src/server_version.txt +++ b/MCPForUnity/UnityMcpServer~/src/server_version.txt @@ -1 +1 @@ -4.1.1 +5.0.0 diff --git a/MCPForUnity/package.json b/MCPForUnity/package.json index b239aca5..20d439e9 100644 --- a/MCPForUnity/package.json +++ b/MCPForUnity/package.json @@ -1,6 +1,6 @@ { "name": "com.coplaydev.unity-mcp", - "version": "4.1.1", + "version": "5.0.0", "displayName": "MCP for Unity", "description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4", "unity": "2021.3",