From ba308daa7a3f7742c27873faa3da468960160cb0 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Mon, 2 Mar 2026 22:49:37 -0800 Subject: [PATCH 01/63] fix: add bearer token environment variable support for Codex agent configuration --- .../UI/AiAgentConfigurators/Impl/CodexConfigurator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/AiAgentConfigurators/Impl/CodexConfigurator.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/AiAgentConfigurators/Impl/CodexConfigurator.cs index 6efda7043..d6905ba00 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/AiAgentConfigurators/Impl/CodexConfigurator.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/AiAgentConfigurators/Impl/CodexConfigurator.cs @@ -137,6 +137,12 @@ protected override void OnUICreated(VisualElement root) var addMcpServerCommandStdio = $"codex mcp add {AiAgentConfig.DefaultMcpServerName} \"{McpServerManager.ExecutableFullPath}\" port={UnityMcpPluginEditor.Port} plugin-timeout={UnityMcpPluginEditor.TimeoutMs} client-transport=stdio"; var addMcpServerCommandHttp = $"codex mcp add {AiAgentConfig.DefaultMcpServerName} --url {UnityMcpPluginEditor.Host}"; + if (UnityMcpPluginEditor.AuthOption == AuthOption.required) + { + addMcpServerCommandStdio += $" --bearer-token-env-var={EnvVarNameAuthToken}"; + addMcpServerCommandHttp += $" --bearer-token-env-var={EnvVarNameAuthToken}"; + } + // STDIO Configuration var manualStepsOption1 = TemplateFoldoutFirst("Manual Configuration Steps - Option 1"); From a24713653481ed8c68006b5aa50a2a6109cdda95 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Mon, 2 Mar 2026 23:03:37 -0800 Subject: [PATCH 02/63] Create bump_version.yml --- .github/workflows/bump_version.yml | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/bump_version.yml diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml new file mode 100644 index 000000000..6e8c8ea8c --- /dev/null +++ b/.github/workflows/bump_version.yml @@ -0,0 +1,37 @@ +name: bump version + +on: + workflow_dispatch: + inputs: + version: + description: "New version number (e.g. 1.2.3)" + required: true + type: string + +jobs: + bump-version: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate version format + run: | + if ! [[ "${{ github.event.inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: version must be in X.X.X format (got: ${{ github.event.inputs.version }})" + exit 1 + fi + + - name: Run bump-version script + shell: pwsh + run: ./commands/bump-version.ps1 -NewVersion "${{ github.event.inputs.version }}" + + - name: Commit version bump + run: | + git config user.name "IvanMurzak" + git config user.email "Ivan.D.Murzak@gmail.com" + git add -A + git commit -m "chore: bump version to ${{ github.event.inputs.version }}" + git push From b428102ff77ba08a1bbd33972599adc9e30a21e1 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Mon, 2 Mar 2026 23:07:47 -0800 Subject: [PATCH 03/63] Update bump_version.yml --- .github/workflows/bump_version.yml | 32 +++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 6e8c8ea8c..4dff6504c 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -12,11 +12,6 @@ jobs: bump-version: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - name: Validate version format run: | if ! [[ "${{ github.event.inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then @@ -24,14 +19,33 @@ jobs: exit 1 fi + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create release branch + run: | + git config user.name "IvanMurzak" + git config user.email "Ivan.D.Murzak@gmail.com" + git checkout -b release/${{ github.event.inputs.version }} + - name: Run bump-version script shell: pwsh run: ./commands/bump-version.ps1 -NewVersion "${{ github.event.inputs.version }}" - - name: Commit version bump + - name: Commit and push version bump run: | - git config user.name "IvanMurzak" - git config user.email "Ivan.D.Murzak@gmail.com" git add -A git commit -m "chore: bump version to ${{ github.event.inputs.version }}" - git push + git push origin release/${{ github.event.inputs.version }} + + - name: Create pull request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --base ${{ github.ref_name }} \ + --head release/${{ github.event.inputs.version }} \ + --title "chore: bump version to ${{ github.event.inputs.version }}" \ + --body "Automated version bump to \`${{ github.event.inputs.version }}\` triggered via workflow dispatch." From 8a46b0ae2c6d2484698e0d9ad9085c7499a52e59 Mon Sep 17 00:00:00 2001 From: IvanMurzak Date: Tue, 3 Mar 2026 07:08:24 +0000 Subject: [PATCH 04/63] chore: bump version to 0.51.4 --- .../Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs | 2 +- README.md | 4 ++-- Unity-MCP-Plugin/Assets/root/README.md | 4 ++-- Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs | 2 +- Unity-MCP-Plugin/Assets/root/package.json | 2 +- Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj | 2 +- Unity-MCP-Server/server.json | 4 ++-- docs/README.es.md | 4 ++-- docs/README.ja.md | 4 ++-- docs/README.zh-CN.md | 4 ++-- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Installer/Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs b/Installer/Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs index 43bdb1794..99d03bcbf 100644 --- a/Installer/Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs +++ b/Installer/Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs @@ -16,7 +16,7 @@ namespace com.IvanMurzak.Unity.MCP.Installer public static partial class Installer { public const string PackageId = "com.ivanmurzak.unity.mcp"; - public const string Version = "0.51.3"; + public const string Version = "0.51.4"; static Installer() { diff --git a/README.md b/README.md index c746ba05b..31755875a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Unlike other tools, this plugin works **inside your compiled game**, allowing fo - ✔️ **Flexible deployment** - Works locally (stdio) and remotely (http) via configuration - ✔️ **Extensible** - Create [custom MCP Tools in your project code](#add-custom-mcp-tool) -[![DOWNLOAD INSTALLER](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage) +[![DOWNLOAD INSTALLER](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) ![AI Game Developer Windows](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -216,7 +216,7 @@ Install extensions when need more tools or [create your own](#add-custom-mcp-too ### Option 1 - Installer -- **[⬇️ Download Installer](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ Download Installer](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** - **📂 Import installer into Unity project** > - You can double-click on the file - Unity will open it automatically > - OR: Open Unity Editor first, then click on `Assets/Import Package/Custom Package`, and choose the file diff --git a/Unity-MCP-Plugin/Assets/root/README.md b/Unity-MCP-Plugin/Assets/root/README.md index c746ba05b..31755875a 100644 --- a/Unity-MCP-Plugin/Assets/root/README.md +++ b/Unity-MCP-Plugin/Assets/root/README.md @@ -37,7 +37,7 @@ Unlike other tools, this plugin works **inside your compiled game**, allowing fo - ✔️ **Flexible deployment** - Works locally (stdio) and remotely (http) via configuration - ✔️ **Extensible** - Create [custom MCP Tools in your project code](#add-custom-mcp-tool) -[![DOWNLOAD INSTALLER](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage) +[![DOWNLOAD INSTALLER](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) ![AI Game Developer Windows](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -216,7 +216,7 @@ Install extensions when need more tools or [create your own](#add-custom-mcp-too ### Option 1 - Installer -- **[⬇️ Download Installer](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ Download Installer](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** - **📂 Import installer into Unity project** > - You can double-click on the file - Unity will open it automatically > - OR: Open Unity Editor first, then click on `Assets/Import Package/Custom Package`, and choose the file diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs index e85afccca..2179729f7 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs @@ -26,7 +26,7 @@ namespace com.IvanMurzak.Unity.MCP public partial class UnityMcpPlugin : IDisposable { - public const string Version = "0.51.3"; + public const string Version = "0.51.4"; private static int _singletonCount = 0; public static bool HasAnyInstance => _singletonCount > 0; diff --git a/Unity-MCP-Plugin/Assets/root/package.json b/Unity-MCP-Plugin/Assets/root/package.json index 3cf6ca8f1..0c702b732 100644 --- a/Unity-MCP-Plugin/Assets/root/package.json +++ b/Unity-MCP-Plugin/Assets/root/package.json @@ -11,7 +11,7 @@ "MCP", "Unity MCP" ], - "version": "0.51.3", + "version": "0.51.4", "unity": "2022.3", "description": "AI-powered bridge connecting LLMs and advanced AI agents to the Unity Editor via the Model Context Protocol (MCP). Chat with AI to generate code, debug errors, and automate game development tasks directly within your project.", "dependencies": { diff --git a/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj b/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj index e8bcb8d91..b8adbc0b1 100644 --- a/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj +++ b/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj @@ -13,7 +13,7 @@ true unity-mcp-server com.IvanMurzak.Unity.MCP.Server - 0.51.3 + 0.51.4 Ivan Murzak Ivan Murzak Copyright © 2025 Ivan Murzak diff --git a/Unity-MCP-Server/server.json b/Unity-MCP-Server/server.json index ff1e10389..fb8208a54 100644 --- a/Unity-MCP-Server/server.json +++ b/Unity-MCP-Server/server.json @@ -8,13 +8,13 @@ "source": "github", "subfolder": "Unity-MCP-Server" }, - "version": "0.51.3", + "version": "0.51.4", "packages": [ { "registry_type": "oci", "registry_base_url": "https://docker.io", "identifier": "ivanmurzakdev/unity-mcp-server", - "version": "0.51.3", + "version": "0.51.4", "transport": { "type": "stdio" }, diff --git a/docs/README.es.md b/docs/README.es.md index 09810ca5d..b2c43f490 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -37,7 +37,7 @@ A diferencia de otras herramientas, este plugin funciona **dentro de tu juego co - ✔️ **Despliegue flexible** - Funciona localmente (stdio) y remotamente (http) mediante configuración - ✔️ **Extensible** - Crea [Herramientas MCP personalizadas en el código de tu proyecto](#añadir-herramienta-mcp-personalizada) -[![DESCARGAR INSTALADOR](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage) +[![DESCARGAR INSTALADOR](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) ![Ventanas del Desarrollador de Juegos con IA](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -216,7 +216,7 @@ Instala extensiones cuando necesites más herramientas o [crea las tuyas propias ### Opción 1 - Instalador -- **[⬇️ Descargar Instalador](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ Descargar Instalador](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** - **📂 Importar el instalador en el proyecto de Unity** > - Puedes hacer doble clic en el archivo - Unity lo abrirá automáticamente > - O BIEN: Abre el Editor de Unity primero, luego haz clic en `Assets/Import Package/Custom Package` y elige el archivo diff --git a/docs/README.ja.md b/docs/README.ja.md index 26edb560c..bf57054ea 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -37,7 +37,7 @@ - ✔️ **柔軟なデプロイ** - 設定によりローカル(stdio)およびリモート(http)で動作 - ✔️ **拡張可能** - [プロジェクトコードにカスタム MCP ツールを作成](#カスタム-mcp-ツールの追加)可能 -[![インストーラーをダウンロード](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage) +[![インストーラーをダウンロード](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) ![AI Game Developer Windows](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -216,7 +216,7 @@ ### オプション 1 - インストーラー -- **[⬇️ インストーラーをダウンロード](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ インストーラーをダウンロード](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** - **📂 Unity プロジェクトにインストーラーをインポート** > - ファイルをダブルクリックすると Unity が自動的に開きます > - または: Unity Editor を先に開き、`Assets/Import Package/Custom Package` をクリックしてファイルを選択 diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index c89c30079..971e3912b 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -37,7 +37,7 @@ - ✔️ **灵活部署** — 支持本地(stdio)和远程(http)两种配置方式 - ✔️ **可扩展** — 在项目代码中[创建自定义 MCP 工具](#添加自定义-mcp-tool) -[![下载安装器](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage) +[![下载安装器](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) ![AI 游戏开发者 Windows](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -216,7 +216,7 @@ ### 选项 1 — 安装器 -- **[⬇️ 下载安装器](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.3/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ 下载安装器](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** - **📂 将安装器导入 Unity 项目** > - 双击文件 — Unity 将自动打开它 > - 或者:先打开 Unity 编辑器,然后点击 `Assets/Import Package/Custom Package`,选择文件 From b50dd5be40e973e2949d77bff9a4860d9c78424c Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Tue, 3 Mar 2026 01:22:59 -0800 Subject: [PATCH 05/63] Replace TryPopulate with TryModify Switch reflection API usage from TryPopulate to TryModify across the plugin: updated editor tools, runtime converters, material/gameobject converters, and unit tests to call TryModify and adjust related method names/log messages. Renamed UnityEngine_Object_ReflectionConverter.Populate.cs to UnityEngine_Object_ReflectionConverter.Modify.cs (and updated its .meta GUID). Binary plugin/reflector DLLs were updated. Also bumped package references in the server project (ReflectorNet -> 4.0.0, McpPlugin.Server -> 5.0.0). This aligns code and tests with the new modify-oriented reflector API and updates diagnostics/comments accordingly. --- .../Editor/Scripts/API/Tool/Assets.Modify.cs | 2 +- .../API/Tool/GameObject.Component.Modify.cs | 2 +- .../Scripts/API/Tool/GameObject.Modify.cs | 2 +- .../Editor/Scripts/API/Tool/Object.Modify.cs | 2 +- .../netstandard2.1/McpPlugin.Common.dll | Bin 53760 -> 53760 bytes .../netstandard2.1/McpPlugin.dll | Bin 171520 -> 171520 bytes .../ReflectorNet.dll | Bin 206848 -> 206848 bytes ...gine_Sprite_ReflectionConverter.Runtime.cs | 4 +-- ...ine_Texture_ReflectionConverter.Runtime.cs | 4 +-- ...Engine_Asset_ReflectionConverter.Editor.cs | 6 ++-- ...ngine_Asset_ReflectionConverter.Runtime.cs | 6 ++-- .../Base/UnityGenericReflectionConverter.cs | 26 +++++++++--------- ...tyEngine_GameObject_ReflectionConverter.cs | 4 +-- ...ine_Material_ReflectionConverter.Editor.cs | 4 +-- ...ne_Material_ReflectionConverter.Runtime.cs | 4 +-- ...nityEngine_Material_ReflectionConverter.cs | 14 +++++----- ...gine_Object_ReflectionConverter.Modify.cs} | 6 ++-- ...Object_ReflectionConverter.Modify.cs.meta} | 8 +++--- .../StructPopulationTests.cs | 24 ++++++++-------- .../root/Tests/Editor/SpriteConverterTest.cs | 2 +- .../Editor/Tool/GameObject/TestSerializer.cs | 2 +- .../com.IvanMurzak.Unity.MCP.Server.csproj | 4 +-- 22 files changed, 63 insertions(+), 63 deletions(-) rename Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/{UnityEngine_Object_ReflectionConverter.Populate.cs => UnityEngine_Object_ReflectionConverter.Modify.cs} (98%) rename Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/{UnityEngine_Object_ReflectionConverter.Populate.cs.meta => UnityEngine_Object_ReflectionConverter.Modify.cs.meta} (60%) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Assets.Modify.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Assets.Modify.cs index c4a906ca9..5ccb40f44 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Assets.Modify.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Assets.Modify.cs @@ -68,7 +68,7 @@ SerializedMember content var logs = new Logs(); var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref obj, data: content, logs: logs, diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Modify.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Modify.cs index d2539290d..adac3c17d 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Modify.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Modify.cs @@ -86,7 +86,7 @@ SerializedMember componentDiff var objToModify = (object)targetComponent; var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Modify.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Modify.cs index 7407e6e4c..fcaa3b5a0 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Modify.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Modify.cs @@ -72,7 +72,7 @@ SerializedMemberList gameObjectDiffs var objToModify = (object)go; var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); - var modified = reflector.TryPopulate( + var modified = reflector.TryModify( ref objToModify, data: gameObjectDiffs[i], logs: logs, diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Object.Modify.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Object.Modify.cs index 487a4dcbd..ca392d30a 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Object.Modify.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Object.Modify.cs @@ -62,7 +62,7 @@ SerializedMember objectDiff var objToModify = (object)obj; var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref objToModify, data: objectDiff, logs: logs, diff --git a/Unity-MCP-Plugin/Assets/root/Plugins/com.IvanMurzak.McpPlugin/netstandard2.1/McpPlugin.Common.dll b/Unity-MCP-Plugin/Assets/root/Plugins/com.IvanMurzak.McpPlugin/netstandard2.1/McpPlugin.Common.dll index 382d7d8f477514f15d2bae03c980b68fd09557af..5b5ccb96f24bfcae0c95ca86f3f0be29803145f4 100644 GIT binary patch delta 246 zcmZoz!rZWgc|r$^qhw_N#-5-mRu%>z*!;LE(nx^Yt>w4PjB1zN3-@FypSbsTv(MTH zQT0?aGc#k8+9q*M++68g6b?#L7-}LRfRJzE>wipZ|gR^x;ge( zG`nvqgBchaGng8r+EYgW delta 246 zcmZoz!rZWgc|r$^uYKjKjXgnCtjr8N42+u}S4A2L97^=j6*?yBKEpPlsg(DB=4PL@ z5u)k_hDj;rhL+|j7N)6*DTaxbmMNANCKl#~CTU5=NofWqmIleGhGv^jADYX=a^Bx= z(d3+?0RmzNjkg*t<#g(K{+@{~{^#1sXO0#sKm|9!1c9oRJcIYT=Pp^!&S-G8W^?SZ zXm(!%21AAO__Q_q0oreXSTym@)ofmazJD9({V|vuA z?K*x;*Dcjk&CJY(;RvFUpzGbJlPMeoBzLF$zy3@`I9_{}GN z^jpSu`zcJa?7pcCW?*Q{V8W2hki=jPCes*F7>s~COCUC4NCZMd24jXaAm0QilLW*D XK$anp4-&NilBpoM?JH+9g);#FYT8kG delta 256 zcmZqJ!_}~dYeEOh+pSY}HTG=nVPxuJWoF=EVB9{ri?Q>tfVhD+W5!(z-!3Vh*AtZZ zEVk?TF!uGPV$ z$IT+XEC2ZPn8{250(YeDvah~4Hyg=k{D7L%z?BekW68)U@&D!Wk>|F41uI25T*dJ1%n9?gH)L? eqycpp192LI0gz`2B$I(U3>nO}ubj;k&IACP5>2Q8 diff --git a/Unity-MCP-Plugin/Assets/root/Plugins/com.IvanMurzak.ReflectorNet/ReflectorNet.dll b/Unity-MCP-Plugin/Assets/root/Plugins/com.IvanMurzak.ReflectorNet/ReflectorNet.dll index 496081dc5ee32f44d6b1179d03e62be5117bd17d..e52004dc8e722647b4cbf5a290857c4eea3b16c7 100644 GIT binary patch delta 47670 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�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 ZSmoc73wb9)d*$ij;p3D~L~CKvUjPeq7`*@h diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/Impl/UnityEngine_Sprite_ReflectionConverter.Runtime.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/Impl/UnityEngine_Sprite_ReflectionConverter.Runtime.cs index f52af3c0a..2059d5370 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/Impl/UnityEngine_Sprite_ReflectionConverter.Runtime.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/Impl/UnityEngine_Sprite_ReflectionConverter.Runtime.cs @@ -24,7 +24,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Converter { public partial class UnityEngine_Sprite_ReflectionConverter : UnityEngine_Asset_ReflectionConverter { - // public override bool TryPopulate( + // public override bool TryModify( // Reflector reflector, // ref object? obj, // SerializedMember data, @@ -37,7 +37,7 @@ public partial class UnityEngine_Sprite_ReflectionConverter : UnityEngine_Asset_ // var padding = StringUtils.GetPadding(depth); // if (logger?.IsEnabled(LogLevel.Trace) == true) - // logger.LogTrace($"{StringUtils.GetPadding(depth)}Populate sprite from data. Converter='{GetType().GetTypeShortName()}'."); + // logger.LogTrace($"{StringUtils.GetPadding(depth)}Modify sprite from data. Converter='{GetType().GetTypeShortName()}'."); // if (logger?.IsEnabled(LogLevel.Error) == true) // logger.LogError($"{padding}Operation is not supported in runtime. Converter: {GetType().GetTypeShortName()}"); diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/Impl/UnityEngine_Texture_ReflectionConverter.Runtime.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/Impl/UnityEngine_Texture_ReflectionConverter.Runtime.cs index 08c409169..a47fd5bca 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/Impl/UnityEngine_Texture_ReflectionConverter.Runtime.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/Impl/UnityEngine_Texture_ReflectionConverter.Runtime.cs @@ -24,7 +24,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Converter { public partial class UnityEngine_Texture_ReflectionConverter : UnityEngine_Asset_ReflectionConverter { - // public override bool TryPopulate( + // public override bool TryModify( // Reflector reflector, // ref object? obj, // SerializedMember data, @@ -37,7 +37,7 @@ public partial class UnityEngine_Texture_ReflectionConverter : UnityEngine_Asset // var padding = StringUtils.GetPadding(depth); // if (logger?.IsEnabled(LogLevel.Trace) == true) - // logger.LogTrace($"{StringUtils.GetPadding(depth)}Populate sprite from data. Converter='{GetType().GetTypeShortName()}'."); + // logger.LogTrace($"{StringUtils.GetPadding(depth)}Modify sprite from data. Converter='{GetType().GetTypeShortName()}'."); // if (logger?.IsEnabled(LogLevel.Error) == true) // logger.LogError($"{padding}Operation is not supported in runtime. Converter: {GetType().GetTypeShortName()}"); diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/UnityEngine_Asset_ReflectionConverter.Editor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/UnityEngine_Asset_ReflectionConverter.Editor.cs index 94d441e33..708e4b301 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/UnityEngine_Asset_ReflectionConverter.Editor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/UnityEngine_Asset_ReflectionConverter.Editor.cs @@ -24,7 +24,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Converter { public partial class UnityEngine_Asset_ReflectionConverter : UnityEngine_Object_ReflectionConverter where T : UnityEngine.Object { - public override bool TryPopulate( + public override bool TryModify( Reflector reflector, ref object? obj, SerializedMember data, @@ -37,7 +37,7 @@ public override bool TryPopulate( var padding = StringUtils.GetPadding(depth); if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{padding}Populate asset from data. Converter='{GetType().GetTypeShortName()}'."); + logger.LogTrace($"{padding}Modify asset from data. Converter='{GetType().GetTypeShortName()}'."); var objectRef = data.valueJsonElement.ToAssetObjectRef( reflector: reflector, @@ -49,7 +49,7 @@ public override bool TryPopulate( { // If no object ref, maybe we should fall back to base behavior? // But for assets, usually we expect an object ref. - // Let's return false to indicate we couldn't populate it as an asset. + // Let's return false to indicate we couldn't modify it as an asset. return false; } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/UnityEngine_Asset_ReflectionConverter.Runtime.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/UnityEngine_Asset_ReflectionConverter.Runtime.cs index bdf7ae1fa..51e77aabb 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/UnityEngine_Asset_ReflectionConverter.Runtime.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Asset/UnityEngine_Asset_ReflectionConverter.Runtime.cs @@ -25,7 +25,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Converter { public partial class UnityEngine_Asset_ReflectionConverter : UnityEngine_Object_ReflectionConverter where T : UnityEngine.Object { - public override bool TryPopulate( + public override bool TryModify( Reflector reflector, ref object? obj, SerializedMember data, @@ -38,7 +38,7 @@ public override bool TryPopulate( var padding = StringUtils.GetPadding(depth); if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{padding}Populate asset from data. Converter='{GetType().GetTypeShortName()}'."); + logger.LogTrace($"{padding}Modify asset from data. Converter='{GetType().GetTypeShortName()}'."); var objectRef = data.valueJsonElement.ToAssetObjectRef( reflector: reflector, @@ -50,7 +50,7 @@ public override bool TryPopulate( { // If no object ref, maybe we should fall back to base behavior? // But for assets, usually we expect an object ref. - // Let's return false to indicate we couldn't populate it as an asset. + // Let's return false to indicate we couldn't modify it as an asset. return false; } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Base/UnityGenericReflectionConverter.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Base/UnityGenericReflectionConverter.cs index 87a07a43f..c68e6fd58 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Base/UnityGenericReflectionConverter.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/Base/UnityGenericReflectionConverter.cs @@ -107,7 +107,7 @@ protected override bool SetValue( logger: logger); // If obj became null but we had an object, and the value didn't explicitly say null, restore it. - // This handles cases where TryPopulate is called with an existing object but no valueJsonElement. + // This handles cases where TryModify is called with an existing object but no valueJsonElement. if (obj == null && originalObj != null) { var isExplicitNull = value.HasValue && value.Value.ValueKind == System.Text.Json.JsonValueKind.Null; @@ -120,7 +120,7 @@ protected override bool SetValue( return result; } - protected override bool TryPopulateField( + protected override bool TryModifyField( Reflector reflector, ref object obj, Type objType, @@ -134,8 +134,8 @@ protected override bool TryPopulateField( if (obj == null) { if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError("{padding}obj is null in TryPopulateField for '{field}'", padding, fieldValue.name); - logs?.Error($"obj is null in TryPopulateField for '{fieldValue.name}'", depth); + logger.LogError("{padding}obj is null in TryModifyField for '{field}'", padding, fieldValue.name); + logs?.Error($"obj is null in TryModifyField for '{fieldValue.name}'", depth); return false; } @@ -152,7 +152,7 @@ protected override bool TryPopulateField( { // For value types (structs) with nested fields/props, we need to: // 1. Get the existing value to preserve unspecified members - // 2. Call TryPopulate on it to modify only the specified members + // 2. Call TryModify on it to modify only the specified members // 3. Write the modified value back to the parent object // This prevents losing existing values when doing partial updates on structs. var fieldType = field.FieldType; @@ -165,8 +165,8 @@ protected override bool TryPopulateField( var existingValue = field.GetValue(obj); if (existingValue != null) { - // Populate the existing struct with only the specified members - var success = reflector.TryPopulate( + // Modify the existing struct with only the specified members + var success = reflector.TryModify( ref existingValue, data: fieldValue, depth: depth + 1, @@ -198,7 +198,7 @@ protected override bool TryPopulateField( } } - protected override bool TryPopulateProperty( + protected override bool TryModifyProperty( Reflector reflector, ref object obj, Type objType, @@ -212,8 +212,8 @@ protected override bool TryPopulateProperty( if (obj == null) { if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError("{padding}obj is null in TryPopulateProperty for '{property}'", padding, member.name); - logs?.Error($"obj is null in TryPopulateProperty for '{member.name}'", depth); + logger.LogError("{padding}obj is null in TryModifyProperty for '{property}'", padding, member.name); + logs?.Error($"obj is null in TryModifyProperty for '{member.name}'", depth); return false; } @@ -230,7 +230,7 @@ protected override bool TryPopulateProperty( { // For value types (structs) with nested fields/props, we need to: // 1. Get the existing value to preserve unspecified members - // 2. Call TryPopulate on it to modify only the specified members + // 2. Call TryModify on it to modify only the specified members // 3. Write the modified value back to the parent object // This prevents losing existing values when doing partial updates on structs. var propertyType = property.PropertyType; @@ -243,8 +243,8 @@ protected override bool TryPopulateProperty( var existingValue = property.GetValue(obj); if (existingValue != null) { - // Populate the existing struct with only the specified members - var success = reflector.TryPopulate( + // Modify the existing struct with only the specified members + var success = reflector.TryModify( ref existingValue, data: member, depth: depth + 1, diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_GameObject_ReflectionConverter.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_GameObject_ReflectionConverter.cs index df9fce596..4618b2453 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_GameObject_ReflectionConverter.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_GameObject_ReflectionConverter.cs @@ -174,7 +174,7 @@ protected override bool SetValue( .FindGameObject(); } - protected override bool TryPopulateProperty( + protected override bool TryModifyProperty( Reflector reflector, ref object obj, Type objType, @@ -210,7 +210,7 @@ protected override bool TryPopulateProperty( logs?.Error($"Failed to set property '{member.name}': {e.Message}", depth); return false; } - return base.TryPopulateProperty( + return base.TryModifyProperty( reflector, ref obj, objType, diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.Editor.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.Editor.cs index 97f0de4dc..d094fe111 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.Editor.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.Editor.cs @@ -27,7 +27,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Converter { public partial class UnityEngine_Material_ReflectionConverter : UnityEngine_Object_ReflectionConverter { - protected override bool TryPopulateProperty( + protected override bool TryModifyProperty( Reflector reflector, ref object obj, Type objType, @@ -40,7 +40,7 @@ protected override bool TryPopulateProperty( var padding = StringUtils.GetPadding(depth); if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{StringUtils.GetPadding(depth)}PopulateProperty property='{propertyValue.name}' type='{propertyValue.typeName}'. Converter='{GetType().GetTypeShortName()}'."); + logger.LogTrace($"{StringUtils.GetPadding(depth)}ModifyProperty property='{propertyValue.name}' type='{propertyValue.typeName}'. Converter='{GetType().GetTypeShortName()}'."); var material = obj as Material; if (material == null) diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.Runtime.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.Runtime.cs index 774c7036f..8fc6260fe 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.Runtime.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.Runtime.cs @@ -25,7 +25,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Converter { public partial class UnityEngine_Material_ReflectionConverter : UnityEngine_Object_ReflectionConverter { - protected override bool TryPopulateProperty( + protected override bool TryModifyProperty( Reflector reflector, ref object obj, Type objType, @@ -38,7 +38,7 @@ protected override bool TryPopulateProperty( var padding = StringUtils.GetPadding(depth); if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{StringUtils.GetPadding(depth)}PopulateProperty property='{propertyValue.name}' type='{propertyValue.typeName}'. Converter='{GetType().GetTypeShortName()}'."); + logger.LogTrace($"{StringUtils.GetPadding(depth)}ModifyProperty property='{propertyValue.name}' type='{propertyValue.typeName}'. Converter='{GetType().GetTypeShortName()}'."); var material = obj as Material; if (material == null) diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.cs index 9c6c8c22f..94ad2b11c 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Material_ReflectionConverter.cs @@ -212,7 +212,7 @@ protected override SerializedMember InternalSerialize( }.SetValue(reflector, new ObjectRef(material)); } - public override bool TryPopulate( + public override bool TryModify( Reflector reflector, ref object? obj, SerializedMember data, @@ -255,7 +255,7 @@ public override bool TryPopulate( if (material.GetInstanceID() == unityObject.GetInstanceID()) { // Recognized as a command to update material - return base.TryPopulate( + return base.TryModify( reflector: reflector, obj: ref obj, data: data, @@ -265,7 +265,7 @@ public override bool TryPopulate( flags: flags, logger: logger); } - // Need to set new material after and maybe to populate the new material. + // Need to set new material after and maybe to modify the new material. var newMaterial = reflector.Deserialize( data, fallbackType: obj?.GetType() ?? typeof(Material), @@ -274,7 +274,7 @@ public override bool TryPopulate( logs: logs, logger: logger); - var success = base.TryPopulate( + var success = base.TryModify( reflector: reflector, obj: ref newMaterial, data: data, @@ -289,7 +289,7 @@ public override bool TryPopulate( return success; } - return base.TryPopulate( + return base.TryModify( reflector: reflector, obj: ref obj, data: data, @@ -300,7 +300,7 @@ public override bool TryPopulate( logger: logger); } - protected override bool TryPopulateField( + protected override bool TryModifyField( Reflector reflector, ref object obj, Type objType, @@ -313,7 +313,7 @@ protected override bool TryPopulateField( var padding = StringUtils.GetPadding(depth); if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{StringUtils.GetPadding(depth)}Populate field for type='{objType.GetTypeId()}'. Converter='{GetType().GetTypeShortName()}'."); + logger.LogTrace($"{StringUtils.GetPadding(depth)}Modify field for type='{objType.GetTypeId()}'. Converter='{GetType().GetTypeShortName()}'."); var material = obj as Material; if (material == null) diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Populate.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Modify.cs similarity index 98% rename from Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Populate.cs rename to Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Modify.cs index 37fc08acb..1d5468e1e 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Populate.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Modify.cs @@ -27,7 +27,7 @@ namespace com.IvanMurzak.Unity.MCP.Reflection.Converter { public partial class UnityEngine_Object_ReflectionConverter { - public override bool TryPopulate( + public override bool TryModify( Reflector reflector, ref object? obj, SerializedMember data, @@ -37,7 +37,7 @@ public override bool TryPopulate( BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, ILogger? logger = null) { - logs?.Info($"TryPopulate called for type '{obj?.GetType().Name}'.", depth); + logs?.Info($"TryModify called for type '{obj?.GetType().Name}'.", depth); // Trying to fix JSON value body, if critical property is missed or detected return false if (!FixJsonValueBody( @@ -52,7 +52,7 @@ public override bool TryPopulate( { return false; } - return base.TryPopulate( + return base.TryModify( reflector: reflector, obj: ref obj, data: data, diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Populate.cs.meta b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Modify.cs.meta similarity index 60% rename from Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Populate.cs.meta rename to Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Modify.cs.meta index 6633fea9f..22d7135df 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Populate.cs.meta +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Converter/Reflection/UnityEngine_Object_ReflectionConverter.Modify.cs.meta @@ -1,11 +1,11 @@ fileFormatVersion: 2 -guid: 154e55a980fc6f84db738be52e562ef5 +guid: 2a3f67b091d74e95ac849cf63f71380d MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/StructPopulationTests.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/StructPopulationTests.cs index 8d7d03646..546b92ef6 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/StructPopulationTests.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/ReflectionConverter/StructPopulationTests.cs @@ -60,14 +60,14 @@ public IEnumerator Populate_Vector3Field_PartialUpdate_PreservesUnspecifiedValue // Act var objToModify = (object)comp; var logs = new Logs(); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert - Assert.IsTrue(success, $"TryPopulate should succeed. Logs: {logs}"); + Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(10f, comp.vector3Field.x, 0.001f, "X should be updated to 10"); Assert.AreEqual(initialVector.y, comp.vector3Field.y, 0.001f, "Y should be preserved"); Assert.AreEqual(initialVector.z, comp.vector3Field.z, 0.001f, "Z should be preserved"); @@ -105,14 +105,14 @@ public IEnumerator Populate_ColorField_PartialUpdate_PreservesUnspecifiedValues( // Act var objToModify = (object)comp; var logs = new Logs(); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert - Assert.IsTrue(success, $"TryPopulate should succeed. Logs: {logs}"); + Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(1.0f, comp.colorField.r, 0.001f, "R should be updated to 1.0"); Assert.AreEqual(0.5f, comp.colorField.g, 0.001f, "G should be updated to 0.5"); Assert.AreEqual(initialColor.b, comp.colorField.b, 0.001f, "B should be preserved"); @@ -156,14 +156,14 @@ public IEnumerator Populate_CustomStruct_PartialUpdate_PreservesUnspecifiedValue // Act var objToModify = (object)comp; var logs = new Logs(); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert - Assert.IsTrue(success, $"TryPopulate should succeed. Logs: {logs}"); + Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(100, comp.customStructField.intValue, "intValue should be updated to 100"); Assert.AreEqual(initialStruct.floatValue, comp.customStructField.floatValue, 0.001f, "floatValue should be preserved"); Assert.AreEqual(initialStruct.stringValue, comp.customStructField.stringValue, "stringValue should be preserved"); @@ -211,14 +211,14 @@ public IEnumerator Populate_NestedStruct_PartialUpdate_PreservesUnspecifiedValue // Act var objToModify = (object)comp; var logs = new Logs(); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert - Assert.IsTrue(success, $"TryPopulate should succeed. Logs: {logs}"); + Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(99f, comp.customStructField.nestedVector.x, 0.001f, "nestedVector.x should be updated to 99"); Assert.AreEqual(initialStruct.nestedVector.y, comp.customStructField.nestedVector.y, 0.001f, "nestedVector.y should be preserved"); Assert.AreEqual(initialStruct.nestedVector.z, comp.customStructField.nestedVector.z, 0.001f, "nestedVector.z should be preserved"); @@ -272,14 +272,14 @@ public IEnumerator Populate_MultipleStructFields_PartialUpdate_PreservesUnspecif // Act var objToModify = (object)comp; var logs = new Logs(); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert - Assert.IsTrue(success, $"TryPopulate should succeed. Logs: {logs}"); + Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); // Verify vector3Field Assert.AreEqual(1f, comp.vector3Field.x, 0.001f, "vector3Field.x should be preserved"); @@ -326,14 +326,14 @@ public IEnumerator Populate_StructField_FullReplacement_Works() // Act var objToModify = (object)comp; var logs = new Logs(); - var success = reflector.TryPopulate( + var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert - Assert.IsTrue(success, $"TryPopulate should succeed. Logs: {logs}"); + Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(newVector.x, comp.vector3Field.x, 0.001f, "vector3Field.x should be updated"); Assert.AreEqual(newVector.y, comp.vector3Field.y, 0.001f, "vector3Field.y should be updated"); Assert.AreEqual(newVector.z, comp.vector3Field.z, 0.001f, "vector3Field.z should be updated"); diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs index 0233d341e..a36394188 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/SpriteConverterTest.cs @@ -79,7 +79,7 @@ public void TestSpritePopulation() // Try to populate object? obj = container; - var result = reflector.TryPopulate(ref obj, data); + var result = reflector.TryModify(ref obj, data); Assert.IsTrue(result, "Population should succeed"); diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestSerializer.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestSerializer.cs index 7e57ef807..a63bfb399 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestSerializer.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestSerializer.cs @@ -102,7 +102,7 @@ public IEnumerator SerializeMaterial() var objMaterial = (object)material; var logs = new ReflectorNet.Model.Logs(); - reflector.TryPopulate( + reflector.TryModify( ref objMaterial, data: serialized, logs: logs, diff --git a/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj b/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj index b8adbc0b1..e54f7ec5f 100644 --- a/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj +++ b/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj @@ -37,8 +37,8 @@ - - + + From 89e764fa0b0e3cc53228aa3d4eb7c37712239edf Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 01:29:50 -0800 Subject: [PATCH 06/63] Update version bump workflow to use force push --- .github/workflows/bump_version.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 4dff6504c..1dd095bf1 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -28,7 +28,8 @@ jobs: run: | git config user.name "IvanMurzak" git config user.email "Ivan.D.Murzak@gmail.com" - git checkout -b release/${{ github.event.inputs.version }} + git fetch origin + git checkout -B release/${{ github.event.inputs.version }} - name: Run bump-version script shell: pwsh @@ -38,7 +39,7 @@ jobs: run: | git add -A git commit -m "chore: bump version to ${{ github.event.inputs.version }}" - git push origin release/${{ github.event.inputs.version }} + git push --force-with-lease origin release/${{ github.event.inputs.version }} - name: Create pull request env: From c87faa7ee8bf409709923d5efbc5e32c1145a2e0 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 12:24:12 -0800 Subject: [PATCH 07/63] Add Unity MCP Test Client Introduce a new Python MCP test client for Unity-MCP. Adds mcp_client.py (async httpx client with Bearer auth, dotenv config, rich-formatted output, schema display and name filtering), README.md with installation and usage instructions, pyproject.toml with project metadata and dependencies, and a .gitignore. Also includes the generated uv.lock lockfile. --- Unity-MCP-Server/MCP-Test-Client/.gitignore | 64 ++ Unity-MCP-Server/MCP-Test-Client/README.md | 181 +++++ .../MCP-Test-Client/mcp_client.py | 242 +++++++ .../MCP-Test-Client/pyproject.toml | 38 ++ Unity-MCP-Server/MCP-Test-Client/uv.lock | 644 ++++++++++++++++++ .../{Server.sln => Unity-MCP-Server.sln} | 4 +- 6 files changed, 1171 insertions(+), 2 deletions(-) create mode 100644 Unity-MCP-Server/MCP-Test-Client/.gitignore create mode 100644 Unity-MCP-Server/MCP-Test-Client/README.md create mode 100644 Unity-MCP-Server/MCP-Test-Client/mcp_client.py create mode 100644 Unity-MCP-Server/MCP-Test-Client/pyproject.toml create mode 100644 Unity-MCP-Server/MCP-Test-Client/uv.lock rename Unity-MCP-Server/{Server.sln => Unity-MCP-Server.sln} (89%) diff --git a/Unity-MCP-Server/MCP-Test-Client/.gitignore b/Unity-MCP-Server/MCP-Test-Client/.gitignore new file mode 100644 index 000000000..40b039b39 --- /dev/null +++ b/Unity-MCP-Server/MCP-Test-Client/.gitignore @@ -0,0 +1,64 @@ +# Environment variables +.env +.env.local +.env.*.local + +# Virtual environment +.venv/ +venv/ +ENV/ +env/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.hypothesis/ + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +*.sublime-project +*.sublime-workspace + +# OS +Thumbs.db +.DS_Store + +# Logs +*.log +logs/ + +# Temporary files +*.tmp +*.temp diff --git a/Unity-MCP-Server/MCP-Test-Client/README.md b/Unity-MCP-Server/MCP-Test-Client/README.md new file mode 100644 index 000000000..8281d9ae5 --- /dev/null +++ b/Unity-MCP-Server/MCP-Test-Client/README.md @@ -0,0 +1,181 @@ +# Unity MCP Test Client + +Python MCP client for connecting to the Unity-MCP Server with colored output and full schema inspection. + +## Installation + +### Prerequisites + +- Python 3.9+ +- [UV package manager](https://docs.astral.sh/uv/getting-started/installation/) (fast, written in Rust) + +### Setup + +```bash +# Install dependencies (creates virtual environment in .venv) +uv sync +``` + +## Configuration + +Edit the `.env` file with your MCP server credentials: + +```env +SERVER_URL=http://localhost:9099 +SERVER_TOKEN=your-token-here +``` + +## Usage + +### Basic Usage + +Display all 56 available MCP tools with their full JSON schemas: + +```bash +uv run python mcp_client.py +``` + +### Filter Tools + +Search for tools by name (case-insensitive): + +```bash +# Show only gameobject-related tools +uv run python mcp_client.py --filter gameobject + +# Show only asset-related tools (short flag) +uv run python mcp_client.py -f assets + +# Show scene tools +uv run python mcp_client.py -f scene + +# Show component tools +uv run python mcp_client.py --filter component +``` + +### Get Help + +```bash +uv run python mcp_client.py --help +``` + +## Output + +The client displays tools in organized panels showing: + +- **Tool Name** — The kebab-case identifier +- **Description** — What the tool does +- **Input Schema** — Complete JSON schema with all properties, types, and requirements +- **Output Schema** — Complete JSON schema of the tool's response +- **Summary Table** — Overview of all tools with schema availability + +## Features + +| Feature | Description | +|---------|-------------| +| 🔗 **Bearer Auth** | Connects via HTTP with Bearer token authentication | +| 📚 **56 Tools** | Fetches all available Unity-MCP tools | +| 📥 **Input Schemas** | Displays complete JSON schemas for tool inputs | +| 📤 **Output Schemas** | Shows response schemas with type definitions | +| 🎨 **Colored Output** | Rich formatting with panels, tables, and syntax highlighting | +| 🔍 **Filtering** | Search tools by name substring | + +## Examples + +### View all tools +```bash +uv run python mcp_client.py +``` + +### Find gameobject tools +```bash +uv run python mcp_client.py -f gameobject +``` + +Output shows 11 matching tools: +- gameobject-component-add +- gameobject-component-list-all +- gameobject-component-remove +- gameobject-create +- gameobject-delete +- gameobject-find +- gameobject-instantiate +- gameobject-move +- gameobject-rename +- gameobject-set-active +- gameobject-set-layer + +### Find asset tools +```bash +uv run python mcp_client.py -f assets +``` + +Output shows 16 matching tools for asset management. + +## UV Commands Reference + +```bash +# Install dependencies +uv sync + +# Add a new package +uv add package-name + +# Add a dev dependency +uv add --dev package-name + +# Activate the virtual environment +source .venv/bin/activate # Linux/macOS +.venv\Scripts\activate # Windows PowerShell + +# Run Python directly in venv +uv run python script.py + +# Specify Python version +uv run --python 3.11 python mcp_client.py + +# View lock file +cat uv.lock + +# Update dependencies +uv lock --upgrade +``` + +## About UV + +UV is a fast Python package manager written in Rust: + +- ⚡ **10-100x faster** than pip/poetry +- 📦 **Lock files** for reproducible builds +- 🐍 **Python version management** built-in +- 🔒 **Secure** by design +- 🌍 **Compatible** with pip/poetry/venv + +[Learn more →](https://docs.astral.sh/uv/) + +## Project Structure + +``` +MCP-Test-Client/ +├── mcp_client.py # Main MCP client script +├── pyproject.toml # Project config and dependencies +├── uv.lock # Dependency lock file +├── .env # Server configuration +├── .venv/ # Virtual environment (auto-created) +└── README.md # This file +``` + +## Troubleshooting + +### Issue: "SERVER_TOKEN not set" +**Fix:** Update your `.env` file with valid credentials. + +### Issue: "Connection refused" +**Fix:** Ensure MCP server is running at the URL specified in `.env`. + +### Issue: "No tools found" +**Fix:** Verify Bearer token is correct and server is responding. + +## License + +MIT diff --git a/Unity-MCP-Server/MCP-Test-Client/mcp_client.py b/Unity-MCP-Server/MCP-Test-Client/mcp_client.py new file mode 100644 index 000000000..e012cb9b8 --- /dev/null +++ b/Unity-MCP-Server/MCP-Test-Client/mcp_client.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +""" +MCP Client for connecting to Unity-MCP Server +Fetches and displays all tools with their JSON schemas using colored output +""" + +import argparse +import asyncio +import json +import os +import sys +from pathlib import Path + +import httpx +from dotenv import load_dotenv +from rich.console import Console +from rich.panel import Panel +from rich.syntax import Syntax +from rich.table import Table +from rich.tree import Tree + +# Fix Windows encoding issues +if sys.platform == "win32": + os.environ["PYTHONIOENCODING"] = "utf-8" + # Force UTF-8 for stdout/stderr + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +class MCPClient: + """MCP Client using HTTP transport with Bearer token auth""" + + def __init__(self, server_url: str, server_token: str): + self.server_url = server_url.rstrip('/') + self.server_token = server_token + self.console = Console(force_terminal=True) + self.headers = { + "Authorization": f"Bearer {server_token}", + "Content-Type": "application/json" + } + self.client = None + self.request_id = 1 + + async def connect(self) -> bool: + """Establish connection to MCP server""" + self.client = httpx.AsyncClient(headers=self.headers, timeout=30.0) + try: + # Test connection by sending an initialization request + self.console.print(f"🔗 Connecting to MCP Server at [bold cyan]{self.server_url}[/bold cyan]...") + + # Try to get server info - MCP servers might use specific endpoints + for endpoint in ["/", "/mcp", "/api", "/tools"]: + try: + response = await self.client.get(f"{self.server_url}{endpoint}") + if response.status_code in [200, 400]: + # If we get 400, the endpoint exists but might need specific format + # We can still try to fetch tools + self.console.print("[green]✓ Connected to MCP Server[/green]") + return True + except Exception: + continue + + # If no endpoint responded, assume we can still try to fetch tools + self.console.print("[yellow]⚠ Could not verify connection, attempting to fetch tools...[/yellow]") + return True + + except Exception as e: + self.console.print(f"[red]✗ Connection error: {e}[/red]") + return False + + async def fetch_tools(self) -> list: + """Fetch all available MCP tools with their schemas""" + try: + # Try different endpoints and methods + endpoints_to_try = [ + ("GET", f"{self.server_url}/tools"), + ("GET", f"{self.server_url}/api/tools"), + ("POST", f"{self.server_url}/mcp"), + ] + + for method, url in endpoints_to_try: + try: + if method == "GET": + response = await self.client.get(url) + else: + payload = { + "jsonrpc": "2.0", + "id": self.request_id, + "method": "tools/list", + "params": {} + } + self.request_id += 1 + response = await self.client.post(url, json=payload) + + if response.status_code == 200: + data = response.json() + + # Try various key patterns + if "result" in data and "tools" in data["result"]: + return data["result"]["tools"] + elif "tools" in data: + return data["tools"] + elif isinstance(data, list): + return data + elif "data" in data and isinstance(data["data"], list): + return data["data"] + except Exception as e: + continue + + self.console.print(f"[yellow]⚠ Could not find tools from any endpoint[/yellow]") + return [] + + except Exception as e: + self.console.print(f"[red]Error fetching tools: {e}[/red]") + return [] + + def print_tools(self, tools: list, filter_str: str = None): + """Print tools with full schemas using colored output + + Args: + tools: List of tools to display + filter_str: Optional substring to filter tools by name + """ + if not tools: + self.console.print("[yellow]No tools found[/yellow]") + return + + # Filter tools if filter string provided + if filter_str: + tools = [t for t in tools if filter_str.lower() in t.get("name", "").lower()] + if not tools: + self.console.print(f"[yellow]No tools match filter: '{filter_str}'[/yellow]") + return + + self.console.print(f"\n[bold magenta]📚 Available Tools ({len(tools)} total)[/bold magenta]\n") + + for i, tool in enumerate(tools, 1): + # Create a panel for each tool + tool_name = tool.get("name", "Unknown") + tool_description = tool.get("description", "No description") + + # Create input schema display + input_schema = tool.get("inputSchema", {}) + output_schema = tool.get("outputSchema", {}) + + # Build tool info content + content = f"[bold]Description:[/bold]\n{tool_description}\n\n" + + # Input Schema + if input_schema: + content += "[bold cyan]📥 Input Schema:[/bold cyan]\n" + schema_json = json.dumps(input_schema, indent=2) + content += f"[dim]{schema_json}[/dim]\n\n" + else: + content += "[dim]No input schema[/dim]\n\n" + + # Output Schema + if output_schema: + content += "[bold green]📤 Output Schema:[/bold green]\n" + schema_json = json.dumps(output_schema, indent=2) + content += f"[dim]{schema_json}[/dim]" + else: + content += "[dim]No output schema[/dim]" + + # Create panel with tool info + panel = Panel( + content, + title=f"[bold yellow]{i}. {tool_name}[/bold yellow]", + border_style="blue", + expand=False + ) + self.console.print(panel) + + # Summary table + self.console.print("\n[bold magenta]📊 Tools Summary[/bold magenta]\n") + table = Table(title="Tool Overview", show_header=True, header_style="bold magenta") + table.add_column("Tool Name", style="cyan") + table.add_column("Has Input Schema", style="green") + table.add_column("Has Output Schema", style="blue") + + for tool in tools: + tool_name = tool.get("name", "Unknown") + has_input = "✓" if tool.get("inputSchema") else "✗" + has_output = "✓" if tool.get("outputSchema") else "✗" + table.add_row(tool_name, has_input, has_output) + + self.console.print(table) + + async def disconnect(self): + """Close connection""" + if self.client: + await self.client.aclose() + self.console.print("\n[green]✓ Disconnected from MCP Server[/green]") + + +async def main(): + """Main entry point""" + + # Parse command-line arguments + parser = argparse.ArgumentParser( + description="MCP Client for connecting to Unity-MCP Server" + ) + parser.add_argument( + "--filter", + "-f", + type=str, + default=None, + help="Filter tools by name substring (case-insensitive)" + ) + args = parser.parse_args() + + # Load environment variables + load_dotenv() + + server_url = os.getenv("SERVER_URL", "http://localhost:8080") + server_token = os.getenv("SERVER_TOKEN", "") + + if not server_token: + console = Console() + console.print("[red]Error: SERVER_TOKEN not set in .env file[/red]") + sys.exit(1) + + # Create and connect client + client = MCPClient(server_url, server_token) + + if not await client.connect(): + sys.exit(1) + + try: + # Fetch and display tools + tools = await client.fetch_tools() + client.print_tools(tools, filter_str=args.filter) + + except KeyboardInterrupt: + print("\n") + finally: + await client.disconnect() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/Unity-MCP-Server/MCP-Test-Client/pyproject.toml b/Unity-MCP-Server/MCP-Test-Client/pyproject.toml new file mode 100644 index 000000000..85e3cfcbe --- /dev/null +++ b/Unity-MCP-Server/MCP-Test-Client/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "unity-mcp-client" +version = "0.1.0" +description = "MCP Client for connecting to Unity-MCP Server" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "MIT"} +authors = [ + {name = "Your Name", email = "your.email@example.com"} +] + +dependencies = [ + "httpx>=0.25.0", + "python-dotenv>=1.0.0", + "rich>=13.0.0", +] + +[project.scripts] +mcp-client = "mcp_client:main" + +[dependency-groups] +dev = [ + "pytest>=7.0", + "pytest-asyncio>=0.21.0", + "black>=23.0.0", + "ruff>=0.1.0", +] + +[tool.uv] +package = false + +[tool.black] +line-length = 100 +target-version = ["py39"] + +[tool.ruff] +line-length = 100 +target-version = "py39" diff --git a/Unity-MCP-Server/MCP-Test-Client/uv.lock b/Unity-MCP-Server/MCP-Test-Client/uv.lock new file mode 100644 index 000000000..3d4845820 --- /dev/null +++ b/Unity-MCP-Server/MCP-Test-Client/uv.lock @@ -0,0 +1,644 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pathspec", marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytokens", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/5b2c0e3215fe748fcf515c2dd34658973a1210bf610e24de5ba887e4f1c8/black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", size = 1743063, upload-time = "2025-11-10T02:02:43.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/245164c6efc27333409c62ba54dcbfbe866c6d1957c9a6c0647786e950da/black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", size = 1596867, upload-time = "2025-11-10T02:00:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/1a3859a7da205f3d50cf3a8bec6bdc551a91c33ae77a045bb24c1f46ab54/black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", size = 1655678, upload-time = "2025-11-10T01:57:09.028Z" }, + { url = "https://files.pythonhosted.org/packages/56/1a/6dec1aeb7be90753d4fcc273e69bc18bfd34b353223ed191da33f7519410/black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", size = 1347452, upload-time = "2025-11-10T01:57:01.871Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + +[[package]] +name = "black" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pathspec", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytokens", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, + { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, + { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, + { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[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, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +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, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[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, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/51/2a/f125667ce48105bf1f4e50e03cfa7b24b8c4f47684d7f1cf4dcb6f6b1c15/pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3", size = 161464, upload-time = "2026-01-30T01:03:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/40/df/065a30790a7ca6bb48ad9018dd44668ed9135610ebf56a2a4cb8e513fd5c/pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1", size = 246159, upload-time = "2026-01-30T01:03:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1c/fd09976a7e04960dabc07ab0e0072c7813d566ec67d5490a4c600683c158/pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db", size = 259120, upload-time = "2026-01-30T01:03:41.233Z" }, + { url = "https://files.pythonhosted.org/packages/52/49/59fdc6fc5a390ae9f308eadeb97dfc70fc2d804ffc49dd39fc97604622ec/pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1", size = 262196, upload-time = "2026-01-30T01:03:42.696Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/d6734dccf0080e3dc00a55b0827ab5af30c886f8bc127bbc04bc3445daec/pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a", size = 103510, upload-time = "2026-01-30T01:03:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, + { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, + { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "unity-mcp-client" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "black", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.25.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "rich", specifier = ">=13.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=23.0.0" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "pytest-asyncio", specifier = ">=0.21.0" }, + { name = "ruff", specifier = ">=0.1.0" }, +] diff --git a/Unity-MCP-Server/Server.sln b/Unity-MCP-Server/Unity-MCP-Server.sln similarity index 89% rename from Unity-MCP-Server/Server.sln rename to Unity-MCP-Server/Unity-MCP-Server.sln index c9628b7a1..ec00afd89 100644 --- a/Unity-MCP-Server/Server.sln +++ b/Unity-MCP-Server/Unity-MCP-Server.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.2.0 MinimumVisualStudioVersion = 10.0.40219.1 @@ -19,6 +19,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {612F1B18-88E0-42C0-BC62-DEC1A689BCEC} + SolutionGuid = {9709159B-BD15-4386-87C0-6767E0C1B321} EndGlobalSection EndGlobal From 6ce6269708b90fbac7f8038433ec9f52d3ced8d7 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 12:25:44 -0800 Subject: [PATCH 08/63] Create .env.example --- Unity-MCP-Server/MCP-Test-Client/.env.example | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Unity-MCP-Server/MCP-Test-Client/.env.example diff --git a/Unity-MCP-Server/MCP-Test-Client/.env.example b/Unity-MCP-Server/MCP-Test-Client/.env.example new file mode 100644 index 000000000..f597bb163 --- /dev/null +++ b/Unity-MCP-Server/MCP-Test-Client/.env.example @@ -0,0 +1,3 @@ +# MCP Server Configuration +SERVER_URL=http://localhost:9099 +SERVER_TOKEN=your_token_here From 8be5c6189d8111a9ad1036870b7e781d49982362 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 11:58:50 -0800 Subject: [PATCH 09/63] Refactor GameObject.Component.Get to improve validation logic and add new tests for input schema validation --- .../API/Tool/GameObject.Component.Get.cs | 12 +-- .../Editor/Tool/GameObject/TestJsonSchema.cs | 101 +++++++++++++++++- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Get.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Get.cs index f3c11bd6f..147129fc6 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Get.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Get.cs @@ -50,14 +50,14 @@ public GetComponentResponse GetComponent bool deepSerialization = false ) { - return MainThread.Instance.Run(() => - { - if (!gameObjectRef.IsValid(out var gameObjectValidationError)) - throw new ArgumentException(gameObjectValidationError, nameof(gameObjectRef)); + if (!gameObjectRef.IsValid(out var gameObjectValidationError)) + throw new ArgumentException(gameObjectValidationError, nameof(gameObjectRef)); - if (!componentRef.IsValid(out var componentValidationError)) - throw new ArgumentException(componentValidationError, nameof(componentRef)); + if (!componentRef.IsValid(out var componentValidationError)) + throw new ArgumentException(componentValidationError, nameof(componentRef)); + return MainThread.Instance.Run(() => + { var go = gameObjectRef.FindGameObject(out var error); if (error != null) throw new Exception(error); diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestJsonSchema.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestJsonSchema.cs index a27074f96..e69c13cdf 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestJsonSchema.cs @@ -15,7 +15,6 @@ using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; -using com.IvanMurzak.McpPlugin.Common; using com.IvanMurzak.McpPlugin.Common.Model; using com.IvanMurzak.ReflectorNet; using com.IvanMurzak.ReflectorNet.Model; @@ -196,5 +195,105 @@ public IEnumerator MCP_Tools() } Assert.IsTrue(toolNames.Count > 0, "No tools found in the response"); } + + [UnityTest] + public IEnumerator MCP_Tools_WithInputArgs_InputSchema_HasTypeObject() + { + var task = UnityMcpPluginEditor.Instance.Tools!.RunListTool(new RequestListTool()); + while (!task.IsCompleted) + { + yield return null; + } + var toolResponse = task.Result; + var tools = toolResponse.Value; + + Assert.IsNotNull(tools, "Tool response is null"); + Assert.IsNotEmpty(tools, "Tool response is empty"); + + foreach (var tool in tools!) + { + var schema = JsonNode.Parse(tool.InputSchema.ToString()); + + if (schema is not JsonObject schemaObject) + { + UnityEngine.Debug.Log($"Skipping tool '{tool.Name}': InputSchema is not a JsonObject"); + continue; + } + + // Filter out tools with no input arguments + var hasInputArgs = schemaObject.TryGetPropertyValue(JsonSchema.Properties, out var propertiesNode) + && propertiesNode is JsonObject propertiesObject + && propertiesObject.Count > 0; + + if (!hasInputArgs) + { + UnityEngine.Debug.Log($"Skipping tool '{tool.Name}': no input arguments"); + continue; + } + + // Tools with input arguments must have "type": "object" in InputSchema + UnityEngine.Debug.Log($"Validating tool '{tool.Name}' InputSchema: {schema}"); + + Assert.IsTrue( + schemaObject.TryGetPropertyValue(JsonSchema.Type, out var typeNode), + $"Tool '{tool.Name}' has input arguments but InputSchema is missing '{JsonSchema.Type}' property. Schema:\n{schema}" + ); + Assert.AreEqual( + "object", + typeNode?.ToString(), + $"Tool '{tool.Name}' InputSchema '{JsonSchema.Type}' is not 'object'. Schema:\n{schema}" + ); + } + } + + [UnityTest] + public IEnumerator MCP_Tools_WithNoInputArgs_InputSchema_HasTypeObject() + { + var task = UnityMcpPluginEditor.Instance.Tools!.RunListTool(new RequestListTool()); + while (!task.IsCompleted) + { + yield return null; + } + var toolResponse = task.Result; + var tools = toolResponse.Value; + + Assert.IsNotNull(tools, "Tool response is null"); + Assert.IsNotEmpty(tools, "Tool response is empty"); + + foreach (var tool in tools!) + { + var schema = JsonNode.Parse(tool.InputSchema.ToString()); + + if (schema is not JsonObject schemaObject) + { + UnityEngine.Debug.Log($"Skipping tool '{tool.Name}': InputSchema is not a JsonObject"); + continue; + } + + // Only test tools with no input arguments + var hasInputArgs = schemaObject.TryGetPropertyValue(JsonSchema.Properties, out var propertiesNode) + && propertiesNode is JsonObject propertiesObject + && propertiesObject.Count > 0; + + if (hasInputArgs) + { + UnityEngine.Debug.Log($"Skipping tool '{tool.Name}': has input arguments"); + continue; + } + + // Tools with no input arguments must still have "type": "object" in InputSchema + UnityEngine.Debug.Log($"Validating tool '{tool.Name}' InputSchema: {schema}"); + + Assert.IsTrue( + schemaObject.TryGetPropertyValue(JsonSchema.Type, out var typeNode), + $"Tool '{tool.Name}' has no input arguments but InputSchema is missing '{JsonSchema.Type}' property. Schema:\n{schema}" + ); + Assert.AreEqual( + "object", + typeNode?.ToString(), + $"Tool '{tool.Name}' InputSchema '{JsonSchema.Type}' is not 'object'. Schema:\n{schema}" + ); + } + } } } From fc0c01b4099320fac95612fa572b09dd267cd36e Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 12:14:29 -0800 Subject: [PATCH 10/63] Refactor validation logic in GameObject.Component.Add and GameObject.Component.Modify to improve error handling --- .../Scripts/API/Tool/GameObject.Component.Add.cs | 12 ++++++------ .../API/Tool/GameObject.Component.Modify.cs | 12 ++++++------ .../Editor/Scripts/API/Tool/GameObject.Modify.cs | 14 +++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Add.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Add.cs index 27fa1ee63..550bda6d3 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Add.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Add.cs @@ -44,6 +44,12 @@ GameObjectRef gameObjectRef if (!gameObjectRef.IsValid(out var gameObjectValidationError)) throw new ArgumentException(gameObjectValidationError, nameof(gameObjectRef)); + if (componentNames == null) + throw new ArgumentNullException(nameof(componentNames), "No component names provided."); + + if (componentNames.Length == 0) + throw new ArgumentException("No component names provided.", nameof(componentNames)); + return MainThread.Instance.Run(() => { var go = gameObjectRef.FindGameObject(out var error); @@ -53,12 +59,6 @@ GameObjectRef gameObjectRef if (go == null) throw new Exception("GameObject not found."); - if (componentNames == null) - throw new ArgumentNullException(nameof(componentNames), "No component names provided."); - - if (componentNames.Length == 0) - throw new ArgumentException("No component names provided.", nameof(componentNames)); - var response = new AddComponentResponse(); foreach (var componentName in componentNames) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Modify.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Modify.cs index adac3c17d..9da31195a 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Modify.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Component.Modify.cs @@ -44,14 +44,14 @@ public ModifyComponentResponse ModifyComponent SerializedMember componentDiff ) { - return MainThread.Instance.Run(() => - { - if (!gameObjectRef.IsValid(out var gameObjectValidationError)) - throw new ArgumentException(gameObjectValidationError, nameof(gameObjectRef)); + if (!gameObjectRef.IsValid(out var gameObjectValidationError)) + throw new ArgumentException(gameObjectValidationError, nameof(gameObjectRef)); - if (!componentRef.IsValid(out var componentValidationError)) - throw new ArgumentException(componentValidationError, nameof(componentRef)); + if (!componentRef.IsValid(out var componentValidationError)) + throw new ArgumentException(componentValidationError, nameof(componentRef)); + return MainThread.Instance.Run(() => + { var go = gameObjectRef.FindGameObject(out var error); if (error != null) throw new Exception(error); diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Modify.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Modify.cs index fcaa3b5a0..8063abee3 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Modify.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/GameObject.Modify.cs @@ -44,15 +44,15 @@ public Logs? Modify SerializedMemberList gameObjectDiffs ) { - return MainThread.Instance.Run(() => - { - if (gameObjectRefs.Count == 0) - throw new Exception("No GameObject references provided. Please provide at least one GameObject reference."); + if (gameObjectRefs.Count == 0) + throw new ArgumentException("No GameObject references provided. Please provide at least one GameObject reference.", nameof(gameObjectRefs)); - if (gameObjectDiffs.Count != gameObjectRefs.Count) - throw new Exception($"The number of {nameof(gameObjectDiffs)} and {nameof(gameObjectRefs)} should be the same. " + - $"{nameof(gameObjectDiffs)}: {gameObjectDiffs.Count}, {nameof(gameObjectRefs)}: {gameObjectRefs.Count}"); + if (gameObjectDiffs.Count != gameObjectRefs.Count) + throw new ArgumentException($"The number of {nameof(gameObjectDiffs)} and {nameof(gameObjectRefs)} should be the same. " + + $"{nameof(gameObjectDiffs)}: {gameObjectDiffs.Count}, {nameof(gameObjectRefs)}: {gameObjectRefs.Count}", nameof(gameObjectDiffs)); + return MainThread.Instance.Run(() => + { var logs = new Logs(); for (int i = 0; i < gameObjectRefs.Count; i++) From 8216ab5dcb8fa5df650366be4c59f910040449ee Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 12:21:24 -0800 Subject: [PATCH 11/63] Remove redundant tests for MCP tools input schema validation --- .../Editor/Tool/GameObject/TestJsonSchema.cs | 100 ------------------ 1 file changed, 100 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestJsonSchema.cs b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestJsonSchema.cs index e69c13cdf..74e30e223 100644 --- a/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +++ b/Unity-MCP-Plugin/Assets/root/Tests/Editor/Tool/GameObject/TestJsonSchema.cs @@ -195,105 +195,5 @@ public IEnumerator MCP_Tools() } Assert.IsTrue(toolNames.Count > 0, "No tools found in the response"); } - - [UnityTest] - public IEnumerator MCP_Tools_WithInputArgs_InputSchema_HasTypeObject() - { - var task = UnityMcpPluginEditor.Instance.Tools!.RunListTool(new RequestListTool()); - while (!task.IsCompleted) - { - yield return null; - } - var toolResponse = task.Result; - var tools = toolResponse.Value; - - Assert.IsNotNull(tools, "Tool response is null"); - Assert.IsNotEmpty(tools, "Tool response is empty"); - - foreach (var tool in tools!) - { - var schema = JsonNode.Parse(tool.InputSchema.ToString()); - - if (schema is not JsonObject schemaObject) - { - UnityEngine.Debug.Log($"Skipping tool '{tool.Name}': InputSchema is not a JsonObject"); - continue; - } - - // Filter out tools with no input arguments - var hasInputArgs = schemaObject.TryGetPropertyValue(JsonSchema.Properties, out var propertiesNode) - && propertiesNode is JsonObject propertiesObject - && propertiesObject.Count > 0; - - if (!hasInputArgs) - { - UnityEngine.Debug.Log($"Skipping tool '{tool.Name}': no input arguments"); - continue; - } - - // Tools with input arguments must have "type": "object" in InputSchema - UnityEngine.Debug.Log($"Validating tool '{tool.Name}' InputSchema: {schema}"); - - Assert.IsTrue( - schemaObject.TryGetPropertyValue(JsonSchema.Type, out var typeNode), - $"Tool '{tool.Name}' has input arguments but InputSchema is missing '{JsonSchema.Type}' property. Schema:\n{schema}" - ); - Assert.AreEqual( - "object", - typeNode?.ToString(), - $"Tool '{tool.Name}' InputSchema '{JsonSchema.Type}' is not 'object'. Schema:\n{schema}" - ); - } - } - - [UnityTest] - public IEnumerator MCP_Tools_WithNoInputArgs_InputSchema_HasTypeObject() - { - var task = UnityMcpPluginEditor.Instance.Tools!.RunListTool(new RequestListTool()); - while (!task.IsCompleted) - { - yield return null; - } - var toolResponse = task.Result; - var tools = toolResponse.Value; - - Assert.IsNotNull(tools, "Tool response is null"); - Assert.IsNotEmpty(tools, "Tool response is empty"); - - foreach (var tool in tools!) - { - var schema = JsonNode.Parse(tool.InputSchema.ToString()); - - if (schema is not JsonObject schemaObject) - { - UnityEngine.Debug.Log($"Skipping tool '{tool.Name}': InputSchema is not a JsonObject"); - continue; - } - - // Only test tools with no input arguments - var hasInputArgs = schemaObject.TryGetPropertyValue(JsonSchema.Properties, out var propertiesNode) - && propertiesNode is JsonObject propertiesObject - && propertiesObject.Count > 0; - - if (hasInputArgs) - { - UnityEngine.Debug.Log($"Skipping tool '{tool.Name}': has input arguments"); - continue; - } - - // Tools with no input arguments must still have "type": "object" in InputSchema - UnityEngine.Debug.Log($"Validating tool '{tool.Name}' InputSchema: {schema}"); - - Assert.IsTrue( - schemaObject.TryGetPropertyValue(JsonSchema.Type, out var typeNode), - $"Tool '{tool.Name}' has no input arguments but InputSchema is missing '{JsonSchema.Type}' property. Schema:\n{schema}" - ); - Assert.AreEqual( - "object", - typeNode?.ToString(), - $"Tool '{tool.Name}' InputSchema '{JsonSchema.Type}' is not 'object'. Schema:\n{schema}" - ); - } - } } } From 42f052315c92200cd15974688741d0e3bcf6fe56 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 13:45:51 -0800 Subject: [PATCH 12/63] Update claude.yml --- .github/workflows/claude.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 5dabd4490..a02c9142f 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -37,6 +37,32 @@ jobs: with: fetch-depth: 1 + - name: Cache Unity Library + uses: actions/cache@v4 + with: + path: | + Unity-MCP-Plugin/Library + ~/.cache/unity3d + key: unity-library-2022.3.63f1-ubuntu-base + + - name: Generate Unity image name + id: unity_image + run: echo "image=unityci/editor:ubuntu-2022.3.63f1-base-3" >> $GITHUB_OUTPUT + shell: bash + + - name: Open Unity project + uses: game-ci/unity-builder@v4 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + with: + projectPath: Unity-MCP-Plugin + unityVersion: 2022.3.63f1 + customImage: ${{ steps.unity_image.outputs.image }} + buildMethod: '' + allowDirtyBuild: true + - name: Run Claude Code id: claude uses: anthropics/claude-code-action@v1 From 7c57e96ac015f21846150df5d688ce44078808be Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 13:55:14 -0800 Subject: [PATCH 13/63] Run Unity editor in Docker, remove unity-builder Replace the game-ci unity-builder step with an explicit Docker run of the unityci/editor image. The workflow now writes the Unity license file into /root/.local/share/unity3d, starts a detached unity-editor container with the repository mounted, launches the project in batchmode, and polls container logs for readiness (with a ~120s timeout). Also simplify the cache path formatting and remove the prior step that generated a custom Unity image output. This gives more direct control over the editor startup and readiness detection in CI. --- .github/workflows/claude.yml | 45 +++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index a02c9142f..58cbc3a52 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -40,28 +40,35 @@ jobs: - name: Cache Unity Library uses: actions/cache@v4 with: - path: | - Unity-MCP-Plugin/Library - ~/.cache/unity3d + path: Unity-MCP-Plugin/Library key: unity-library-2022.3.63f1-ubuntu-base - - name: Generate Unity image name - id: unity_image - run: echo "image=unityci/editor:ubuntu-2022.3.63f1-base-3" >> $GITHUB_OUTPUT - shell: bash + - name: Start Unity Editor in background + run: | + # Write license file required by unityci images + mkdir -p /root/.local/share/unity3d/Unity + echo "${{ secrets.UNITY_LICENSE }}" > /root/.local/share/unity3d/Unity/Unity_lic.ulf - - name: Open Unity project - uses: game-ci/unity-builder@v4 - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - with: - projectPath: Unity-MCP-Plugin - unityVersion: 2022.3.63f1 - customImage: ${{ steps.unity_image.outputs.image }} - buildMethod: '' - allowDirtyBuild: true + docker run -d \ + --name unity-editor \ + -v "${GITHUB_WORKSPACE}:/workspace" \ + -v /root/.local/share/unity3d:/root/.local/share/unity3d \ + unityci/editor:ubuntu-2022.3.63f1-base-3 \ + unity-editor \ + -batchmode \ + -projectPath /workspace/Unity-MCP-Plugin \ + -logFile /dev/stdout + + echo "Waiting for Unity project to load..." + # Poll until the MCP plugin signals readiness or timeout after 120 s + for i in $(seq 1 24); do + sleep 5 + if docker logs unity-editor 2>&1 | grep -q "MCP.*ready\|Compilation finished\|All assemblies built"; then + echo "Unity is ready."; break + fi + echo "Still waiting... ($((i*5))s)" + done + shell: bash - name: Run Claude Code id: claude From c408e68e3bd34158957c452c2c177732e949891b Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 14:09:50 -0800 Subject: [PATCH 14/63] Add env/CLI config overrides and docs Add support for overriding Unity MCP plugin connection settings via environment variables and command-line args. Implemented EnvironmentUtils.ApplyEnvironmentOverrides (with constants for UNITY_MCP_HOST, UNITY_MCP_KEEP_CONNECTED, UNITY_MCP_AUTH_OPTION, UNITY_MCP_TOKEN) and wire it into the editor config loader. Update README and localized docs (and the plugin README) with a new "Plugin Variables" section and example usage; also add a CLAUDE.md rule requiring the plugin README to mirror the root README. Overrides are applied at startup, not persisted, and command-line args take precedence over environment variables. --- CLAUDE.md | 6 +++ README.md | 24 +++++++++++ .../Scripts/UnityMcpPluginEditor.Config.cs | 2 + Unity-MCP-Plugin/Assets/root/README.md | 24 +++++++++++ .../root/Runtime/Utils/EnvironmentUtils.cs | 41 +++++++++++++++++++ docs/README.es.md | 24 +++++++++++ docs/README.ja.md | 24 +++++++++++ docs/README.zh-CN.md | 24 +++++++++++ 8 files changed, 169 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index f0794b53d..aee10fa4f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,3 +86,9 @@ These apply across both C# sub-projects: - All Unity API calls must use `MainThread.Instance.Run(() => ...)` or `RunAsync()` - Tool/prompt names use **kebab-case** with category prefix (e.g., `gameobject-create`, `assets-find`) - Namespace pattern: `com.IvanMurzak.Unity.MCP.[Tier].[Component]` + +## Rules + +Important rules that must be followed: + +- `./Unity-MCP-Plugin/Assets/root/README.md` must be a copy of `./README.md`. diff --git a/README.md b/README.md index 31755875a..ca8bf1490 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,7 @@ Install extensions when need more tools or [create your own](#add-custom-mcp-too - [Why runtime usage is needed?](#why-runtime-usage-is-needed) - [Unity `MCP Server` setup](#unity-mcp-server-setup) - [Variables](#variables) + - [Plugin Variables](#plugin-variables) - [Docker 📦](#docker-) - [`streamableHttp` Transport](#streamablehttp-transport) - [`stdio` Transport](#stdio-transport) @@ -503,6 +504,29 @@ Doesn't matter what launch option you choose, all of them support custom configu > **Choosing a transport:** Use `stdio` when the MCP client launches the server binary directly (local use — this is the most common setup). Use `streamableHttp` when running the server as a standalone process or in Docker/cloud, and connecting over HTTP. +## Plugin Variables + +The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are **not persisted** to disk — they apply for the current session only. + +| Environment Variable | Command Line Arg | Values | Description | +| --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Override the MCP Server host URL | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Force enable or disable the active connection | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Force set the authentication mode | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Force set the authentication token | + +> Command-line args take precedence over environment variables. Both override the saved config file value. + +**Example (CI/CD batch mode):** + +```bash +Unity.exe -batchmode -nographics \ + -UNITY_MCP_HOST=http://localhost:8080 \ + -UNITY_MCP_KEEP_CONNECTED=true \ + -UNITY_MCP_AUTH_OPTION=required \ + -UNITY_MCP_TOKEN=my-secret-token +``` + ## Docker 📦 [![Docker Image](https://img.shields.io/docker/image-size/ivanmurzakdev/unity-mcp-server/latest?label=Docker%20Image&logo=docker&labelColor=333A41 'Docker Image')](https://hub.docker.com/r/ivanmurzakdev/unity-mcp-server) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs index dfb326bc9..d424060bf 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs @@ -13,6 +13,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using com.IvanMurzak.Unity.MCP.Runtime.Utils; using Microsoft.Extensions.Logging; using UnityEngine; @@ -98,6 +99,7 @@ UnityConnectionConfig GetOrCreateConfig(out bool wasCreated) wasCreated = true; } + EnvironmentUtils.ApplyEnvironmentOverrides(config); return config; } catch (Exception e) diff --git a/Unity-MCP-Plugin/Assets/root/README.md b/Unity-MCP-Plugin/Assets/root/README.md index 31755875a..ca8bf1490 100644 --- a/Unity-MCP-Plugin/Assets/root/README.md +++ b/Unity-MCP-Plugin/Assets/root/README.md @@ -170,6 +170,7 @@ Install extensions when need more tools or [create your own](#add-custom-mcp-too - [Why runtime usage is needed?](#why-runtime-usage-is-needed) - [Unity `MCP Server` setup](#unity-mcp-server-setup) - [Variables](#variables) + - [Plugin Variables](#plugin-variables) - [Docker 📦](#docker-) - [`streamableHttp` Transport](#streamablehttp-transport) - [`stdio` Transport](#stdio-transport) @@ -503,6 +504,29 @@ Doesn't matter what launch option you choose, all of them support custom configu > **Choosing a transport:** Use `stdio` when the MCP client launches the server binary directly (local use — this is the most common setup). Use `streamableHttp` when running the server as a standalone process or in Docker/cloud, and connecting over HTTP. +## Plugin Variables + +The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are **not persisted** to disk — they apply for the current session only. + +| Environment Variable | Command Line Arg | Values | Description | +| --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Override the MCP Server host URL | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Force enable or disable the active connection | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Force set the authentication mode | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Force set the authentication token | + +> Command-line args take precedence over environment variables. Both override the saved config file value. + +**Example (CI/CD batch mode):** + +```bash +Unity.exe -batchmode -nographics \ + -UNITY_MCP_HOST=http://localhost:8080 \ + -UNITY_MCP_KEEP_CONNECTED=true \ + -UNITY_MCP_AUTH_OPTION=required \ + -UNITY_MCP_TOKEN=my-secret-token +``` + ## Docker 📦 [![Docker Image](https://img.shields.io/docker/image-size/ivanmurzakdev/unity-mcp-server/latest?label=Docker%20Image&logo=docker&labelColor=333A41 'Docker Image')](https://hub.docker.com/r/ivanmurzakdev/unity-mcp-server) diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs index 3c2655be7..aa65e7966 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs @@ -12,11 +12,19 @@ using System; using System.Collections.Generic; using com.IvanMurzak.McpPlugin.Common.Utils; +using static com.IvanMurzak.McpPlugin.Common.Consts.MCP.Server; namespace com.IvanMurzak.Unity.MCP.Runtime.Utils { public static class EnvironmentUtils { + // Environment variable names for MCP connection overrides. + // These override values loaded from the JSON config file and are never persisted to disk. + public const string EnvHost = "UNITY_MCP_HOST"; + public const string EnvKeepConnected = "UNITY_MCP_KEEP_CONNECTED"; + public const string EnvAuthOption = "UNITY_MCP_AUTH_OPTION"; + public const string EnvToken = "UNITY_MCP_TOKEN"; + /// /// Checks if the current environment is a CI environment. /// @@ -32,5 +40,38 @@ public static bool IsCi() || string.Equals(gha?.Trim()?.Trim('"'), "true", StringComparison.OrdinalIgnoreCase) || string.Equals(az?.Trim()?.Trim('"'), "true", StringComparison.OrdinalIgnoreCase); } + + /// + /// Applies environment variable (or command-line argument) overrides to the given config. + /// Checks command-line args first, then falls back to process environment variables. + /// Invalid or missing values are silently ignored, leaving the config field unchanged. + /// Overrides are NOT persisted to disk. + /// + public static void ApplyEnvironmentOverrides(UnityMcpPlugin.UnityConnectionConfig config) + { + var args = ArgsUtils.ParseCommandLineArguments(); + + // Host URL override + var host = args.GetValueOrDefault(EnvHost) ?? Environment.GetEnvironmentVariable(EnvHost); + if (!string.IsNullOrWhiteSpace(host)) + config.Host = host.Trim().Trim('"'); + + // KeepConnected (active connection) override + var keepConnected = args.GetValueOrDefault(EnvKeepConnected) ?? Environment.GetEnvironmentVariable(EnvKeepConnected); + if (!string.IsNullOrWhiteSpace(keepConnected) + && bool.TryParse(keepConnected.Trim().Trim('"'), out var kc)) + config.KeepConnected = kc; + + // AuthOption override (none / required) + var authOption = args.GetValueOrDefault(EnvAuthOption) ?? Environment.GetEnvironmentVariable(EnvAuthOption); + if (!string.IsNullOrWhiteSpace(authOption) + && Enum.TryParse(authOption.Trim().Trim('"'), ignoreCase: true, out var ao)) + config.AuthOption = ao; + + // Auth token override + var token = args.GetValueOrDefault(EnvToken) ?? Environment.GetEnvironmentVariable(EnvToken); + if (!string.IsNullOrWhiteSpace(token)) + config.Token = token.Trim().Trim('"'); + } } } diff --git a/docs/README.es.md b/docs/README.es.md index b2c43f490..ca1a27fa4 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -170,6 +170,7 @@ Instala extensiones cuando necesites más herramientas o [crea las tuyas propias - [¿Por qué se necesita el uso en runtime?](#por-qué-se-necesita-el-uso-en-runtime) - [Configuración del `Servidor MCP` de Unity](#configuración-del-servidor-mcp-de-unity) - [Variables](#variables) + - [Variables del Plugin](#variables-del-plugin) - [Docker 📦](#docker-) - [Transporte `streamableHttp`](#transporte-streamablehttp) - [Transporte `stdio`](#transporte-stdio) @@ -503,6 +504,29 @@ Sin importar qué opción de lanzamiento elijas, todas admiten configuración pe > **Elegir un transporte:** Usa `stdio` cuando el cliente MCP lanza el binario del servidor directamente (uso local — esta es la configuración más común). Usa `streamableHttp` cuando ejecutes el servidor como un proceso independiente o en Docker/nube, y te conectes a través de HTTP. +## Variables del Plugin + +El Plugin Unity MCP lee las siguientes variables de entorno (y argumentos de línea de comandos) al arrancar para sobreescribir los valores del archivo de configuración guardado. Las sobreescrituras **no se persisten** en disco — aplican solo para la sesión actual. + +| Variable de Entorno | Arg de Línea de Comandos | Valores | Descripción | +| --------------------------- | --------------------------- | ------------------- | --------------------------------------------------------- | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Sobreescribe la URL del servidor MCP | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Fuerza habilitar o deshabilitar la conexión activa | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Fuerza el modo de autenticación | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Fuerza el token de autenticación | + +> Los argumentos de línea de comandos tienen precedencia sobre las variables de entorno. Ambos sobreescriben el valor del archivo de configuración guardado. + +**Ejemplo (modo batch CI/CD):** + +```bash +Unity.exe -batchmode -nographics \ + -UNITY_MCP_HOST=http://localhost:8080 \ + -UNITY_MCP_KEEP_CONNECTED=true \ + -UNITY_MCP_AUTH_OPTION=required \ + -UNITY_MCP_TOKEN=mi-token-secreto +``` + ## Docker 📦 [![Docker Image](https://img.shields.io/docker/image-size/ivanmurzakdev/unity-mcp-server/latest?label=Docker%20Image&logo=docker&labelColor=333A41 'Imagen Docker')](https://hub.docker.com/r/ivanmurzakdev/unity-mcp-server) diff --git a/docs/README.ja.md b/docs/README.ja.md index bf57054ea..005136924 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -170,6 +170,7 @@ - [なぜランタイム使用が必要か?](#なぜランタイム使用が必要か) - [Unity `MCP Server` のセットアップ](#unity-mcp-server-のセットアップ) - [変数](#変数) + - [プラグイン変数](#プラグイン変数) - [Docker 📦](#docker-) - [`streamableHttp` トランスポート](#streamablehttp-トランスポート) - [`stdio` トランスポート](#stdio-トランスポート) @@ -503,6 +504,29 @@ public static class ChessGameAI > **トランスポートの選択:** MCP クライアントがサーバーバイナリを直接起動する場合(ローカル使用 — 最も一般的な設定)は `stdio` を使用します。サーバーをスタンドアロンプロセスとして実行するか Docker/クラウドで実行して HTTP 経由で接続する場合は `streamableHttp` を使用します。 +## プラグイン変数 + +Unity MCP Plugin は起動時に以下の環境変数(およびコマンドライン引数)を読み込み、保存済み設定ファイルの値を上書きします。上書きはディスクに**保存されません** — 現在のセッションのみ有効です。 + +| 環境変数 | コマンドライン引数 | 値 | 説明 | +| --------------------------- | --------------------------- | ------------------- | ---------------------------------------- | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL 文字列 | MCP サーバーのホスト URL を上書き | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | アクティブ接続を強制的に有効/無効化 | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | 認証モードを強制設定 | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | 文字列 | 認証トークンを強制設定 | + +> コマンドライン引数は環境変数より優先されます。どちらも保存済み設定ファイルの値を上書きします。 + +**例(CI/CD バッチモード):** + +```bash +Unity.exe -batchmode -nographics \ + -UNITY_MCP_HOST=http://localhost:8080 \ + -UNITY_MCP_KEEP_CONNECTED=true \ + -UNITY_MCP_AUTH_OPTION=required \ + -UNITY_MCP_TOKEN=my-secret-token +``` + ## Docker 📦 [![Docker Image](https://img.shields.io/docker/image-size/ivanmurzakdev/unity-mcp-server/latest?label=Docker%20Image&logo=docker&labelColor=333A41 'Docker Image')](https://hub.docker.com/r/ivanmurzakdev/unity-mcp-server) diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 971e3912b..98abcc7b7 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -170,6 +170,7 @@ - [为什么需要运行时使用?](#为什么需要运行时使用) - [Unity `MCP Server` 设置](#unity-mcp-server-设置) - [变量](#变量) + - [插件变量](#插件变量) - [Docker 📦](#docker-) - [`streamableHttp` 传输](#streamablehttp-传输) - [`stdio` 传输](#stdio-传输) @@ -503,6 +504,29 @@ public static class ChessGameAI > **选择传输方式:** 当 MCP 客户端直接启动服务器二进制文件时(本地使用 — 这是最常见的配置),使用 `stdio`。当以独立进程或在 Docker/云端运行服务器并通过 HTTP 连接时,使用 `streamableHttp`。 +## 插件变量 + +Unity MCP 插件在启动时读取以下环境变量(及命令行参数),用于覆盖已保存配置文件中的值。覆盖**不会持久化**到磁盘 — 仅在当前会话有效。 + +| 环境变量 | 命令行参数 | 值 | 描述 | +| --------------------------- | --------------------------- | ------------------- | ------------------------------ | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL 字符串 | 覆盖 MCP 服务器主机 URL | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | 强制启用或禁用活动连接 | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | 强制设置认证模式 | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | 字符串 | 强制设置认证令牌 | + +> 命令行参数优先于环境变量。两者均会覆盖已保存的配置文件值。 + +**示例(CI/CD 批处理模式):** + +```bash +Unity.exe -batchmode -nographics \ + -UNITY_MCP_HOST=http://localhost:8080 \ + -UNITY_MCP_KEEP_CONNECTED=true \ + -UNITY_MCP_AUTH_OPTION=required \ + -UNITY_MCP_TOKEN=my-secret-token +``` + ## Docker 📦 [![Docker Image](https://img.shields.io/docker/image-size/ivanmurzakdev/unity-mcp-server/latest?label=Docker%20Image&logo=docker&labelColor=333A41 'Docker Image')](https://hub.docker.com/r/ivanmurzakdev/unity-mcp-server) From 61b15223cdda57171f7eb4a3bb576e2af4e6dfa1 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 14:20:26 -0800 Subject: [PATCH 15/63] Update claude.yml --- .github/workflows/claude.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 58cbc3a52..2daa723e7 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -43,6 +43,26 @@ jobs: path: Unity-MCP-Plugin/Library key: unity-library-2022.3.63f1-ubuntu-base + - name: Start AI-Game-Developer MCP Server + run: | + docker run -d \ + --name unity-mcp-server \ + --network host \ + -e MCP_PLUGIN_PORT=8080 \ + -e MCP_PLUGIN_CLIENT_TRANSPORT=streamableHttp \ + -e MCP_AUTHORIZATION=none \ + ivanmurzakdev/unity-mcp-server + + echo "Waiting for MCP Server to be ready..." + for i in $(seq 1 12); do + sleep 5 + if curl -sf http://localhost:8080 > /dev/null 2>&1; then + echo "MCP Server is ready."; break + fi + echo "Still waiting... ($((i*5))s)" + done + shell: bash + - name: Start Unity Editor in background run: | # Write license file required by unityci images @@ -51,6 +71,10 @@ jobs: docker run -d \ --name unity-editor \ + --network host \ + -e UNITY_MCP_HOST=http://localhost:8080 \ + -e UNITY_MCP_KEEP_CONNECTED=true \ + -e UNITY_MCP_AUTH_OPTION=none \ -v "${GITHUB_WORKSPACE}:/workspace" \ -v /root/.local/share/unity3d:/root/.local/share/unity3d \ unityci/editor:ubuntu-2022.3.63f1-base-3 \ @@ -95,3 +119,4 @@ jobs: --allowedTools Read --allowedTools WebFetch --allowedTools WebSearch + --mcp-config '{"mcpServers":{"ai-game-developer":{"type":"streamableHttp","url":"http://localhost:8080"}}}' From 24937dddddfef02497dee72eb223b2878ec8a8ab Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 16:42:32 -0800 Subject: [PATCH 16/63] Improve CI workflow: dispatch, Docker robustness Add manual workflow_dispatch trigger and allow manual runs. Harden Docker-based startup for the MCP server and Unity editor by removing stale containers, using log-based readiness checks, adding timeouts and fail-fast checks, and increasing Unity startup wait time. Add conditional Unity start (skip when ACT=true), support UNITY_EMAIL/UNITY_PASSWORD activation flow inside the editor container, and emit license/activation diagnostics. Pass github_token into the claude action and expand allowedTools to grant broader CI inspection and automation capabilities. --- .github/workflows/claude.yml | 68 ++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 2daa723e7..6fb8fc589 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -1,6 +1,7 @@ name: Claude Code on: + workflow_dispatch: issue_comment: types: [created] pull_request_review_comment: @@ -13,6 +14,7 @@ on: jobs: claude: if: | + github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || @@ -45,6 +47,8 @@ jobs: - name: Start AI-Game-Developer MCP Server run: | + docker rm -f unity-mcp-server 2>/dev/null || true + docker run -d \ --name unity-mcp-server \ --network host \ @@ -56,41 +60,63 @@ jobs: echo "Waiting for MCP Server to be ready..." for i in $(seq 1 12); do sleep 5 - if curl -sf http://localhost:8080 > /dev/null 2>&1; then + if docker logs unity-mcp-server 2>&1 | grep -q "Start listening on port:"; then echo "MCP Server is ready."; break fi echo "Still waiting... ($((i*5))s)" + if [ "$i" -eq 12 ]; then + echo "Timeout waiting for MCP Server" + docker logs unity-mcp-server + exit 1 + fi done shell: bash - name: Start Unity Editor in background + if: ${{ env.ACT != 'true' }} + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} run: | - # Write license file required by unityci images - mkdir -p /root/.local/share/unity3d/Unity - echo "${{ secrets.UNITY_LICENSE }}" > /root/.local/share/unity3d/Unity/Unity_lic.ulf + docker rm -f unity-editor 2>/dev/null || true + # Activate Unity online with email+password, then open the project. docker run -d \ --name unity-editor \ --network host \ + -e UNITY_EMAIL \ + -e UNITY_PASSWORD \ -e UNITY_MCP_HOST=http://localhost:8080 \ -e UNITY_MCP_KEEP_CONNECTED=true \ -e UNITY_MCP_AUTH_OPTION=none \ -v "${GITHUB_WORKSPACE}:/workspace" \ - -v /root/.local/share/unity3d:/root/.local/share/unity3d \ + --entrypoint /bin/bash \ unityci/editor:ubuntu-2022.3.63f1-base-3 \ - unity-editor \ - -batchmode \ - -projectPath /workspace/Unity-MCP-Plugin \ - -logFile /dev/stdout + -c 'unity-editor -quit -batchmode -nographics -logFile /dev/stdout \ + -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" ; \ + echo "--- Activation exit: $? ---" ; \ + echo "--- License dir contents: ---" ; \ + ls -la /root/.local/share/unity3d/Unity/ 2>&1 || echo "(dir not found)" ; \ + exec unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout' echo "Waiting for Unity project to load..." - # Poll until the MCP plugin signals readiness or timeout after 120 s - for i in $(seq 1 24); do + for i in $(seq 1 48); do sleep 5 + # Fail fast if the container already exited + if [ "$(docker inspect -f '{{.State.Status}}' unity-editor 2>/dev/null)" = "exited" ]; then + echo "Unity Editor container exited early!" + docker logs unity-editor + exit 1 + fi if docker logs unity-editor 2>&1 | grep -q "MCP.*ready\|Compilation finished\|All assemblies built"; then echo "Unity is ready."; break fi echo "Still waiting... ($((i*5))s)" + if [ "$i" -eq 48 ]; then + echo "Timeout waiting for Unity Editor" + docker logs unity-editor + exit 1 + fi done shell: bash @@ -99,6 +125,7 @@ jobs: uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github_token: ${{ github.token }} # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | @@ -119,4 +146,23 @@ jobs: --allowedTools Read --allowedTools WebFetch --allowedTools WebSearch + --allowedTools Bash(*) + --allowedTools WebBrowser + --allowedTools WebFetch(*) + --allowedTools WebSearch(*) + --allowedTools Read(*) + --allowedTools Write(*) + --allowedTools Edit(*) + --allowedTools Glob(*) + --allowedTools Grep(*) + --allowedTools Agent(*) + --allowedTools NotebookEdit(*) + --allowedTools TodoWrite + --allowedTools Skill(*) + --allowedTools TaskOutput(*) + --allowedTools TaskStop(*) + --allowedTools EnterPlanMode + --allowedTools ExitPlanMode + --allowedTools EnterWorktree + --allowedTools AskUserQuestion --mcp-config '{"mcpServers":{"ai-game-developer":{"type":"streamableHttp","url":"http://localhost:8080"}}}' From f30cd388ff0e0270933440c0272d9f0a588bbaad Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 16:49:01 -0800 Subject: [PATCH 17/63] Update claude.yml --- .github/workflows/claude.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6fb8fc589..b80856930 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -43,7 +43,7 @@ jobs: uses: actions/cache@v4 with: path: Unity-MCP-Plugin/Library - key: unity-library-2022.3.63f1-ubuntu-base + key: unity-library-2022.3.62f3-ubuntu-base - name: Start AI-Game-Developer MCP Server run: | @@ -75,7 +75,7 @@ jobs: - name: Start Unity Editor in background if: ${{ env.ACT != 'true' }} env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} run: | docker rm -f unity-editor 2>/dev/null || true @@ -91,7 +91,7 @@ jobs: -e UNITY_MCP_AUTH_OPTION=none \ -v "${GITHUB_WORKSPACE}:/workspace" \ --entrypoint /bin/bash \ - unityci/editor:ubuntu-2022.3.63f1-base-3 \ + unityci/editor:ubuntu-2022.3.62f3-base-3 \ -c 'unity-editor -quit -batchmode -nographics -logFile /dev/stdout \ -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" ; \ echo "--- Activation exit: $? ---" ; \ From e505b1f4a3133487012cd94d2d6b4fa71b2bd1ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:17:45 +0000 Subject: [PATCH 18/63] Update README: env/CLI overrides are persistent, not session-only Co-authored-by: IvanMurzak <9135028+IvanMurzak@users.noreply.github.com> --- README.md | 2 +- Unity-MCP-Plugin/Assets/root/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ca8bf1490..6006e5c37 100644 --- a/README.md +++ b/README.md @@ -506,7 +506,7 @@ Doesn't matter what launch option you choose, all of them support custom configu ## Plugin Variables -The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are **not persisted** to disk — they apply for the current session only. +The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides **are persisted** to disk — on first run or when a new authentication token is generated, the overridden values are written to the config file and will be used in subsequent sessions. | Environment Variable | Command Line Arg | Values | Description | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | diff --git a/Unity-MCP-Plugin/Assets/root/README.md b/Unity-MCP-Plugin/Assets/root/README.md index ca8bf1490..6006e5c37 100644 --- a/Unity-MCP-Plugin/Assets/root/README.md +++ b/Unity-MCP-Plugin/Assets/root/README.md @@ -506,7 +506,7 @@ Doesn't matter what launch option you choose, all of them support custom configu ## Plugin Variables -The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are **not persisted** to disk — they apply for the current session only. +The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides **are persisted** to disk — on first run or when a new authentication token is generated, the overridden values are written to the config file and will be used in subsequent sessions. | Environment Variable | Command Line Arg | Values | Description | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | From ae5d1b4f6aff2345ddeffe57d18c34f1c6682ced Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 17:22:24 -0800 Subject: [PATCH 19/63] CI: Add Unity activation and env logging Update GitHub workflow to use a cache key derived from ProjectVersion.txt and add an explicit Unity license activation Docker step (using UNITY_EMAIL/UNITY_PASSWORD secrets) so activation runs synchronously before launching the editor. Simplify the editor startup command, adjust allowed Claude tools ordering, and add a cleanup step to remove lingering Docker containers. In the Unity plugin, add Microsoft.Extensions.Logging and UnityLoggerFactory usage in EnvironmentUtils to log environment variable overrides for MCP settings (host, keepConnected, auth option, token) for better observability. --- .github/workflows/claude.yml | 54 ++++++++++++------- .../root/Runtime/Utils/EnvironmentUtils.cs | 15 ++++++ 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index b80856930..f2a741b6d 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -43,7 +43,7 @@ jobs: uses: actions/cache@v4 with: path: Unity-MCP-Plugin/Library - key: unity-library-2022.3.62f3-ubuntu-base + key: unity-library-${{ hashFiles('Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt') }}-ubuntu-base - name: Start AI-Game-Developer MCP Server run: | @@ -72,6 +72,30 @@ jobs: done shell: bash + - name: Activate Unity license + if: ${{ env.ACT != 'true' }} + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + run: | + docker rm -f unity-activate 2>/dev/null || true + + # Run activation synchronously — exits with Unity's exit code. + docker run --rm \ + --name unity-activate \ + --network host \ + -e UNITY_EMAIL \ + -e UNITY_PASSWORD \ + -v "${GITHUB_WORKSPACE}:/workspace" \ + unityci/editor:ubuntu-2022.3.62f3-base-3 \ + unity-editor -quit -batchmode -nographics -logFile /dev/stdout \ + -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" + + echo "--- Activation exit: $? ---" + echo "--- License dir contents: ---" + ls -la /root/.local/share/unity3d/Unity/ 2>&1 || echo "(dir not found)" + shell: bash + - name: Start Unity Editor in background if: ${{ env.ACT != 'true' }} env: @@ -80,7 +104,6 @@ jobs: run: | docker rm -f unity-editor 2>/dev/null || true - # Activate Unity online with email+password, then open the project. docker run -d \ --name unity-editor \ --network host \ @@ -90,14 +113,8 @@ jobs: -e UNITY_MCP_KEEP_CONNECTED=true \ -e UNITY_MCP_AUTH_OPTION=none \ -v "${GITHUB_WORKSPACE}:/workspace" \ - --entrypoint /bin/bash \ unityci/editor:ubuntu-2022.3.62f3-base-3 \ - -c 'unity-editor -quit -batchmode -nographics -logFile /dev/stdout \ - -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" ; \ - echo "--- Activation exit: $? ---" ; \ - echo "--- License dir contents: ---" ; \ - ls -la /root/.local/share/unity3d/Unity/ 2>&1 || echo "(dir not found)" ; \ - exec unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout' + unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout echo "Waiting for Unity project to load..." for i in $(seq 1 48); do @@ -138,23 +155,14 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options claude_args: >- - --allowedTools Bash - --allowedTools Edit - --allowedTools Write - --allowedTools Glob - --allowedTools Grep - --allowedTools Read - --allowedTools WebFetch - --allowedTools WebSearch --allowedTools Bash(*) - --allowedTools WebBrowser - --allowedTools WebFetch(*) - --allowedTools WebSearch(*) --allowedTools Read(*) --allowedTools Write(*) --allowedTools Edit(*) --allowedTools Glob(*) --allowedTools Grep(*) + --allowedTools WebFetch(*) + --allowedTools WebSearch(*) --allowedTools Agent(*) --allowedTools NotebookEdit(*) --allowedTools TodoWrite @@ -166,3 +174,9 @@ jobs: --allowedTools EnterWorktree --allowedTools AskUserQuestion --mcp-config '{"mcpServers":{"ai-game-developer":{"type":"streamableHttp","url":"http://localhost:8080"}}}' + + - name: Cleanup Docker containers + if: always() + run: | + docker rm -f unity-mcp-server unity-editor 2>/dev/null || true + shell: bash diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs index aa65e7966..ca484fd1c 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs @@ -12,12 +12,15 @@ using System; using System.Collections.Generic; using com.IvanMurzak.McpPlugin.Common.Utils; +using com.IvanMurzak.Unity.MCP.Utils; +using Microsoft.Extensions.Logging; using static com.IvanMurzak.McpPlugin.Common.Consts.MCP.Server; namespace com.IvanMurzak.Unity.MCP.Runtime.Utils { public static class EnvironmentUtils { + static readonly ILogger _logger = UnityLoggerFactory.LoggerFactory.CreateLogger(nameof(EnvironmentUtils)); // Environment variable names for MCP connection overrides. // These override values loaded from the JSON config file and are never persisted to disk. public const string EnvHost = "UNITY_MCP_HOST"; @@ -54,24 +57,36 @@ public static void ApplyEnvironmentOverrides(UnityMcpPlugin.UnityConnectionConfi // Host URL override var host = args.GetValueOrDefault(EnvHost) ?? Environment.GetEnvironmentVariable(EnvHost); if (!string.IsNullOrWhiteSpace(host)) + { config.Host = host.Trim().Trim('"'); + _logger.LogInformation("[MCP] Env override: {Key}={Value}", EnvHost, config.Host); + } // KeepConnected (active connection) override var keepConnected = args.GetValueOrDefault(EnvKeepConnected) ?? Environment.GetEnvironmentVariable(EnvKeepConnected); if (!string.IsNullOrWhiteSpace(keepConnected) && bool.TryParse(keepConnected.Trim().Trim('"'), out var kc)) + { config.KeepConnected = kc; + _logger.LogInformation("[MCP] Env override: {Key}={Value}", EnvKeepConnected, config.KeepConnected); + } // AuthOption override (none / required) var authOption = args.GetValueOrDefault(EnvAuthOption) ?? Environment.GetEnvironmentVariable(EnvAuthOption); if (!string.IsNullOrWhiteSpace(authOption) && Enum.TryParse(authOption.Trim().Trim('"'), ignoreCase: true, out var ao)) + { config.AuthOption = ao; + _logger.LogInformation("[MCP] Env override: {Key}={Value}", EnvAuthOption, config.AuthOption); + } // Auth token override var token = args.GetValueOrDefault(EnvToken) ?? Environment.GetEnvironmentVariable(EnvToken); if (!string.IsNullOrWhiteSpace(token)) + { config.Token = token.Trim().Trim('"'); + _logger.LogInformation("[MCP] Env override: {Key}=***", EnvToken); + } } } } From 2b8f44af7c6ba5a40cadc39fcd57ba1737b665b8 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 17:33:41 -0800 Subject: [PATCH 20/63] Update CLAUDE.md --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index aee10fa4f..a3eb4be9a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,3 +92,4 @@ These apply across both C# sub-projects: Important rules that must be followed: - `./Unity-MCP-Plugin/Assets/root/README.md` must be a copy of `./README.md`. +- `./Unity-MCP-Plugin/Assets/root/README.md` must be translated to related translated versions of this file under `./docs/README.*.md`. From 75c6bbff0814d3bba8f51f6ccadc38a5ebbc9ba8 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 17:49:12 -0800 Subject: [PATCH 21/63] Update claude.yml --- .github/workflows/claude.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index f2a741b6d..b503aeb94 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -39,11 +39,17 @@ jobs: with: fetch-depth: 1 + - name: Read Unity version + run: | + UNITY_VERSION=$(grep "^m_EditorVersion:" Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt | awk '{print $2}') + echo "UNITY_VERSION=${UNITY_VERSION}" >> $GITHUB_ENV + shell: bash + - name: Cache Unity Library uses: actions/cache@v4 with: path: Unity-MCP-Plugin/Library - key: unity-library-${{ hashFiles('Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt') }}-ubuntu-base + key: unity-library-${{ env.UNITY_VERSION }}-ubuntu-base - name: Start AI-Game-Developer MCP Server run: | @@ -73,7 +79,6 @@ jobs: shell: bash - name: Activate Unity license - if: ${{ env.ACT != 'true' }} env: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} @@ -87,7 +92,7 @@ jobs: -e UNITY_EMAIL \ -e UNITY_PASSWORD \ -v "${GITHUB_WORKSPACE}:/workspace" \ - unityci/editor:ubuntu-2022.3.62f3-base-3 \ + unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ unity-editor -quit -batchmode -nographics -logFile /dev/stdout \ -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" @@ -97,7 +102,6 @@ jobs: shell: bash - name: Start Unity Editor in background - if: ${{ env.ACT != 'true' }} env: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} @@ -113,7 +117,7 @@ jobs: -e UNITY_MCP_KEEP_CONNECTED=true \ -e UNITY_MCP_AUTH_OPTION=none \ -v "${GITHUB_WORKSPACE}:/workspace" \ - unityci/editor:ubuntu-2022.3.62f3-base-3 \ + unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout echo "Waiting for Unity project to load..." From b92fbaaa757af90a762188cadec6f26f5e59322e Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 4 Mar 2026 21:25:04 -0800 Subject: [PATCH 22/63] Enhance MCP Plugin: Add support for ENABLED_TOOLS environment variable and update documentation --- README.md | 9 +++-- .../root/Runtime/UnityMcpPlugin.Build.cs | 39 ++++++++++++++++--- .../root/Runtime/UnityMcpPlugin.Config.cs | 9 +++++ .../root/Runtime/Utils/EnvironmentUtils.cs | 13 +++++++ docs/README.es.md | 9 +++-- docs/README.ja.md | 9 +++-- docs/README.zh-CN.md | 9 +++-- 7 files changed, 75 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 6006e5c37..3044fa121 100644 --- a/README.md +++ b/README.md @@ -510,10 +510,11 @@ The Unity MCP Plugin reads the following environment variables (and command-line | Environment Variable | Command Line Arg | Values | Description | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | -| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Override the MCP Server host URL | -| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Force enable or disable the active connection | -| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Force set the authentication mode | -| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Force set the authentication token | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Override the MCP Server host URL | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Force enable or disable the active connection | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Force set the authentication mode | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Force set the authentication token | +| `UNITY_MCP_TOOLS` | `-UNITY_MCP_TOOLS` | comma-separated tool IDs | Enable only the listed tools; all others are disabled. Unknown IDs are logged as errors. | > Command-line args take precedence over environment variables. Both override the saved config file value. diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs index 22df7a7b9..48db9a8ca 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs @@ -10,6 +10,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Linq; using com.IvanMurzak.McpPlugin; using com.IvanMurzak.ReflectorNet; @@ -164,13 +165,39 @@ protected virtual void ApplyConfigToMcpPlugin(IMcpPlugin mcpPlugin) var toolManager = mcpPlugin.McpManager.ToolManager; if (toolManager != null) { - foreach (var tool in toolManager.GetAllTools()) + var enabledToolsOverride = unityConnectionConfig.EnabledToolsOverride; + if (enabledToolsOverride != null) { - var toolFeature = unityConnectionConfig.Tools.FirstOrDefault(t => t.Name == tool.Name!); - var isEnabled = toolFeature == null || toolFeature.Enabled; - toolManager.SetToolEnabled(tool.Name!, isEnabled); - _logger.LogDebug("{method}: Tool '{tool}' enabled: {isEnabled}", - nameof(ApplyConfigToMcpPlugin), tool.Name, isEnabled); + // Validate requested tool IDs against the registered tool list + var allToolNames = new HashSet( + toolManager.GetAllTools().Select(t => t.Name!), + StringComparer.OrdinalIgnoreCase); + foreach (var requestedId in enabledToolsOverride) + { + if (!allToolNames.Contains(requestedId)) + _logger.LogError("[MCP] {Key}: tool '{ToolId}' not found. Check the tool ID.", + "UNITY_MCP_TOOLS", requestedId); + } + + // Apply: enable only tools in the override list, disable all others + foreach (var tool in toolManager.GetAllTools()) + { + var isEnabled = enabledToolsOverride.Contains(tool.Name!); + toolManager.SetToolEnabled(tool.Name!, isEnabled); + _logger.LogDebug("{method}: Tool '{tool}' enabled: {isEnabled} (env override)", + nameof(ApplyConfigToMcpPlugin), tool.Name, isEnabled); + } + } + else + { + foreach (var tool in toolManager.GetAllTools()) + { + var toolFeature = unityConnectionConfig.Tools.FirstOrDefault(t => t.Name == tool.Name!); + var isEnabled = toolFeature == null || toolFeature.Enabled; + toolManager.SetToolEnabled(tool.Name!, isEnabled); + _logger.LogDebug("{method}: Tool '{tool}' enabled: {isEnabled}", + nameof(ApplyConfigToMcpPlugin), tool.Name, isEnabled); + } } } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Config.cs b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Config.cs index 2dcbd1c0b..1b71e18f3 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Config.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Config.cs @@ -10,6 +10,7 @@ #nullable enable using System.Collections.Generic; +using System.Text.Json.Serialization; using com.IvanMurzak.McpPlugin; using com.IvanMurzak.McpPlugin.Common; using com.IvanMurzak.Unity.MCP.Runtime.Utils; @@ -39,6 +40,14 @@ public class UnityConnectionConfig : ConnectionConfig public List Prompts { get; set; } = new(); public List Resources { get; set; } = new(); + /// + /// When non-null, only the tools whose names appear in this list are enabled; + /// all others are disabled. Set by the UNITY_MCP_TOOLS environment variable + /// (comma-separated tool IDs). Not persisted to disk. + /// + [JsonIgnore] + public List? EnabledToolsOverride { get; set; } + public UnityConnectionConfig() { SetDefault(); diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs index ca484fd1c..8424f1564 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs @@ -27,6 +27,7 @@ public static class EnvironmentUtils public const string EnvKeepConnected = "UNITY_MCP_KEEP_CONNECTED"; public const string EnvAuthOption = "UNITY_MCP_AUTH_OPTION"; public const string EnvToken = "UNITY_MCP_TOKEN"; + public const string EnvTools = "UNITY_MCP_TOOLS"; /// /// Checks if the current environment is a CI environment. @@ -87,6 +88,18 @@ public static void ApplyEnvironmentOverrides(UnityMcpPlugin.UnityConnectionConfi config.Token = token.Trim().Trim('"'); _logger.LogInformation("[MCP] Env override: {Key}=***", EnvToken); } + + // Enabled tools override — comma-separated tool IDs + var tools = args.GetValueOrDefault(EnvTools) ?? Environment.GetEnvironmentVariable(EnvTools); + if (!string.IsNullOrWhiteSpace(tools)) + { + var ids = tools.Split(',', StringSplitOptions.RemoveEmptyEntries); + var trimmed = new List(ids.Length); + foreach (var id in ids) + trimmed.Add(id.Trim().Trim('"')); + config.EnabledToolsOverride = trimmed; + _logger.LogInformation("[MCP] Env override: {Key}={Value}", EnvTools, tools.Trim()); + } } } } diff --git a/docs/README.es.md b/docs/README.es.md index ca1a27fa4..0b39d2dfc 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -510,10 +510,11 @@ El Plugin Unity MCP lee las siguientes variables de entorno (y argumentos de lí | Variable de Entorno | Arg de Línea de Comandos | Valores | Descripción | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------------------- | -| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Sobreescribe la URL del servidor MCP | -| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Fuerza habilitar o deshabilitar la conexión activa | -| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Fuerza el modo de autenticación | -| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Fuerza el token de autenticación | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Sobreescribe la URL del servidor MCP | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Fuerza habilitar o deshabilitar la conexión activa | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Fuerza el modo de autenticación | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Fuerza el token de autenticación | +| `UNITY_MCP_TOOLS` | `-UNITY_MCP_TOOLS` | IDs separados por comas | Activa solo las herramientas listadas; todas las demás se desactivan. Los IDs desconocidos se registran como errores. | > Los argumentos de línea de comandos tienen precedencia sobre las variables de entorno. Ambos sobreescriben el valor del archivo de configuración guardado. diff --git a/docs/README.ja.md b/docs/README.ja.md index 005136924..832a6a6f8 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -510,10 +510,11 @@ Unity MCP Plugin は起動時に以下の環境変数(およびコマンドラ | 環境変数 | コマンドライン引数 | 値 | 説明 | | --------------------------- | --------------------------- | ------------------- | ---------------------------------------- | -| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL 文字列 | MCP サーバーのホスト URL を上書き | -| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | アクティブ接続を強制的に有効/無効化 | -| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | 認証モードを強制設定 | -| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | 文字列 | 認証トークンを強制設定 | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL 文字列 | MCP サーバーのホスト URL を上書き | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | アクティブ接続を強制的に有効/無効化 | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | 認証モードを強制設定 | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | 文字列 | 認証トークンを強制設定 | +| `UNITY_MCP_TOOLS` | `-UNITY_MCP_TOOLS` | カンマ区切りのツール ID | 指定したツールのみ有効化し、それ以外はすべて無効化します。不明な ID はエラーとしてログに記録されます。 | > コマンドライン引数は環境変数より優先されます。どちらも保存済み設定ファイルの値を上書きします。 diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 98abcc7b7..8cf765fa4 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -510,10 +510,11 @@ Unity MCP 插件在启动时读取以下环境变量(及命令行参数), | 环境变量 | 命令行参数 | 值 | 描述 | | --------------------------- | --------------------------- | ------------------- | ------------------------------ | -| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL 字符串 | 覆盖 MCP 服务器主机 URL | -| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | 强制启用或禁用活动连接 | -| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | 强制设置认证模式 | -| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | 字符串 | 强制设置认证令牌 | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL 字符串 | 覆盖 MCP 服务器主机 URL | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | 强制启用或禁用活动连接 | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | 强制设置认证模式 | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | 字符串 | 强制设置认证令牌 | +| `UNITY_MCP_TOOLS` | `-UNITY_MCP_TOOLS` | 逗号分隔的工具 ID | 仅启用列出的工具,其余全部禁用。未知 ID 将记录为错误日志。 | > 命令行参数优先于环境变量。两者均会覆盖已保存的配置文件值。 From 5e266dfd64f194b9329e5da202fc203cd3c4fdd4 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 5 Mar 2026 01:25:18 -0800 Subject: [PATCH 23/63] Update Unity MCP Plugin: Refactor environment variable handling and improve documentation for persistence --- .github/workflows/claude.yml | 7 +++++-- .../root/Editor/Scripts/UnityMcpPluginEditor.Config.cs | 1 - .../Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs | 2 ++ .../Assets/root/Runtime/Utils/EnvironmentUtils.cs | 5 +++-- docs/README.es.md | 2 +- docs/README.ja.md | 2 +- docs/README.zh-CN.md | 2 +- 7 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index b503aeb94..c5a4712e6 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -40,16 +40,17 @@ jobs: fetch-depth: 1 - name: Read Unity version + id: unity-version run: | UNITY_VERSION=$(grep "^m_EditorVersion:" Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt | awk '{print $2}') - echo "UNITY_VERSION=${UNITY_VERSION}" >> $GITHUB_ENV + echo "unity_version=${UNITY_VERSION}" >> "$GITHUB_OUTPUT" shell: bash - name: Cache Unity Library uses: actions/cache@v4 with: path: Unity-MCP-Plugin/Library - key: unity-library-${{ env.UNITY_VERSION }}-ubuntu-base + key: unity-library-${{ steps.unity-version.outputs.unity_version }}-ubuntu-base - name: Start AI-Game-Developer MCP Server run: | @@ -82,6 +83,7 @@ jobs: env: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} run: | docker rm -f unity-activate 2>/dev/null || true @@ -105,6 +107,7 @@ jobs: env: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} run: | docker rm -f unity-editor 2>/dev/null || true diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs index d424060bf..396b33e93 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs @@ -99,7 +99,6 @@ UnityConnectionConfig GetOrCreateConfig(out bool wasCreated) wasCreated = true; } - EnvironmentUtils.ApplyEnvironmentOverrides(config); return config; } catch (Exception e) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs index da0eefb69..15531c41a 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs @@ -9,6 +9,7 @@ */ #nullable enable +using com.IvanMurzak.Unity.MCP.Runtime.Utils; namespace com.IvanMurzak.Unity.MCP { @@ -26,6 +27,7 @@ protected UnityMcpPluginEditor() : base() ApplyLogLevel(unityConnectionConfig.LogLevel); if (wasCreated) Save(); + EnvironmentUtils.ApplyEnvironmentOverrides(unityConnectionConfig); IncrementSingletonCount(); } diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs index 8424f1564..839aa5508 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs @@ -22,7 +22,8 @@ public static class EnvironmentUtils { static readonly ILogger _logger = UnityLoggerFactory.LoggerFactory.CreateLogger(nameof(EnvironmentUtils)); // Environment variable names for MCP connection overrides. - // These override values loaded from the JSON config file and are never persisted to disk. + // These override values loaded from the JSON config file. Overrides are applied after + // any initial Save() so they are never written back to disk. public const string EnvHost = "UNITY_MCP_HOST"; public const string EnvKeepConnected = "UNITY_MCP_KEEP_CONNECTED"; public const string EnvAuthOption = "UNITY_MCP_AUTH_OPTION"; @@ -49,7 +50,7 @@ public static bool IsCi() /// Applies environment variable (or command-line argument) overrides to the given config. /// Checks command-line args first, then falls back to process environment variables. /// Invalid or missing values are silently ignored, leaving the config field unchanged. - /// Overrides are NOT persisted to disk. + /// Overrides are applied after any initial config save and are never written back to disk. /// public static void ApplyEnvironmentOverrides(UnityMcpPlugin.UnityConnectionConfig config) { diff --git a/docs/README.es.md b/docs/README.es.md index 0b39d2dfc..b587e606e 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -506,7 +506,7 @@ Sin importar qué opción de lanzamiento elijas, todas admiten configuración pe ## Variables del Plugin -El Plugin Unity MCP lee las siguientes variables de entorno (y argumentos de línea de comandos) al arrancar para sobreescribir los valores del archivo de configuración guardado. Las sobreescrituras **no se persisten** en disco — aplican solo para la sesión actual. +El Plugin Unity MCP lee las siguientes variables de entorno (y argumentos de línea de comandos) al arrancar para sobreescribir los valores del archivo de configuración guardado. Las sobreescrituras **sí se persisten** en disco — en la primera ejecución o cuando se genera un nuevo token de autenticación, los valores sobreescritos se escriben en el archivo de configuración y se usarán en sesiones posteriores. | Variable de Entorno | Arg de Línea de Comandos | Valores | Descripción | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------------------- | diff --git a/docs/README.ja.md b/docs/README.ja.md index 832a6a6f8..8f6ee73a8 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -506,7 +506,7 @@ public static class ChessGameAI ## プラグイン変数 -Unity MCP Plugin は起動時に以下の環境変数(およびコマンドライン引数)を読み込み、保存済み設定ファイルの値を上書きします。上書きはディスクに**保存されません** — 現在のセッションのみ有効です。 +Unity MCP Plugin は起動時に以下の環境変数(およびコマンドライン引数)を読み込み、保存済み設定ファイルの値を上書きします。上書きはディスクに**保存されます** — 初回起動時または新しい認証トークンが生成された際に、上書きされた値が設定ファイルに書き込まれ、以降のセッションでも使用されます。 | 環境変数 | コマンドライン引数 | 値 | 説明 | | --------------------------- | --------------------------- | ------------------- | ---------------------------------------- | diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 8cf765fa4..c4beade68 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -506,7 +506,7 @@ public static class ChessGameAI ## 插件变量 -Unity MCP 插件在启动时读取以下环境变量(及命令行参数),用于覆盖已保存配置文件中的值。覆盖**不会持久化**到磁盘 — 仅在当前会话有效。 +Unity MCP 插件在启动时读取以下环境变量(及命令行参数),用于覆盖已保存配置文件中的值。覆盖**会持久化**到磁盘 — 在首次运行或生成新认证令牌时,被覆盖的值将写入配置文件,并在后续会话中继续生效。 | 环境变量 | 命令行参数 | 值 | 描述 | | --------------------------- | --------------------------- | ------------------- | ------------------------------ | From d9512c173d44b3e834d0233cebc766c439cf11b2 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 5 Mar 2026 01:29:50 -0800 Subject: [PATCH 24/63] Add Copilot setup steps workflow for Unity MCP --- .github/workflows/copilot-setup-steps.yml | 134 ++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..07036a52d --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,134 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Read Unity version + id: unity-version + run: | + UNITY_VERSION=$(grep "^m_EditorVersion:" Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt | awk '{print $2}') + echo "unity_version=${UNITY_VERSION}" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Cache Unity Library + uses: actions/cache@v4 + with: + path: Unity-MCP-Plugin/Library + key: unity-library-${{ steps.unity-version.outputs.unity_version }}-ubuntu-base + + - name: Start AI-Game-Developer MCP Server + run: | + docker rm -f unity-mcp-server 2>/dev/null || true + + docker run -d \ + --name unity-mcp-server \ + --network host \ + -e MCP_PLUGIN_PORT=8080 \ + -e MCP_PLUGIN_CLIENT_TRANSPORT=streamableHttp \ + -e MCP_AUTHORIZATION=none \ + ivanmurzakdev/unity-mcp-server + + echo "Waiting for MCP Server to be ready..." + for i in $(seq 1 12); do + sleep 5 + if docker logs unity-mcp-server 2>&1 | grep -q "Start listening on port:"; then + echo "MCP Server is ready."; break + fi + echo "Still waiting... ($((i*5))s)" + if [ "$i" -eq 12 ]; then + echo "Timeout waiting for MCP Server" + docker logs unity-mcp-server + exit 1 + fi + done + shell: bash + + - name: Activate Unity license + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} + run: | + docker rm -f unity-activate 2>/dev/null || true + + # Run activation synchronously — exits with Unity's exit code. + docker run --rm \ + --name unity-activate \ + --network host \ + -e UNITY_EMAIL \ + -e UNITY_PASSWORD \ + -v "${GITHUB_WORKSPACE}:/workspace" \ + unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ + unity-editor -quit -batchmode -nographics -logFile /dev/stdout \ + -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" + + echo "--- Activation exit: $? ---" + echo "--- License dir contents: ---" + ls -la /root/.local/share/unity3d/Unity/ 2>&1 || echo "(dir not found)" + shell: bash + + - name: Start Unity Editor in background + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} + run: | + docker rm -f unity-editor 2>/dev/null || true + + docker run -d \ + --name unity-editor \ + --network host \ + -e UNITY_EMAIL \ + -e UNITY_PASSWORD \ + -e UNITY_MCP_HOST=http://localhost:8080 \ + -e UNITY_MCP_KEEP_CONNECTED=true \ + -e UNITY_MCP_AUTH_OPTION=none \ + -v "${GITHUB_WORKSPACE}:/workspace" \ + unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ + unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout + + echo "Waiting for Unity project to load..." + for i in $(seq 1 48); do + sleep 5 + # Fail fast if the container already exited + if [ "$(docker inspect -f '{{.State.Status}}' unity-editor 2>/dev/null)" = "exited" ]; then + echo "Unity Editor container exited early!" + docker logs unity-editor + exit 1 + fi + if docker logs unity-editor 2>&1 | grep -q "MCP.*ready\|Compilation finished\|All assemblies built"; then + echo "Unity is ready."; break + fi + echo "Still waiting... ($((i*5))s)" + if [ "$i" -eq 48 ]; then + echo "Timeout waiting for Unity Editor" + docker logs unity-editor + exit 1 + fi + done + shell: bash \ No newline at end of file From 5c5e0e29aaf718388dd5181605a64c77edddd628 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 14:15:22 -0800 Subject: [PATCH 25/63] Enhance CI workflows: Setup Node.js, install unity-license-activate, and improve Unity license activation process --- .github/workflows/claude.yml | 66 ++++++++++++++++------- .github/workflows/copilot-setup-steps.yml | 66 ++++++++++++++++------- 2 files changed, 94 insertions(+), 38 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index c5a4712e6..c8c7cbbec 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -79,34 +79,63 @@ jobs: done shell: bash - - name: Activate Unity license + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install unity-license-activate + run: npm install --global unity-license-activate + shell: bash + + - name: Generate Unity activation file (.alf) env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} run: | - docker rm -f unity-activate 2>/dev/null || true + mkdir -p "${GITHUB_WORKSPACE}/.unity-license" - # Run activation synchronously — exits with Unity's exit code. docker run --rm \ - --name unity-activate \ - --network host \ - -e UNITY_EMAIL \ - -e UNITY_PASSWORD \ - -v "${GITHUB_WORKSPACE}:/workspace" \ + -v "${GITHUB_WORKSPACE}/.unity-license:/output" \ + -w /output \ unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ - unity-editor -quit -batchmode -nographics -logFile /dev/stdout \ - -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" + unity-editor -batchmode -createManualActivationFile -logFile /dev/stdout || true + + ALF_FILE=$(find "${GITHUB_WORKSPACE}/.unity-license" -name "*.alf" | head -1) + if [ -z "$ALF_FILE" ]; then + echo "Failed to generate .alf file" + exit 1 + fi + echo "Generated ALF file: $ALF_FILE" + echo "alf_path=${ALF_FILE}" >> "$GITHUB_OUTPUT" + id: alf + shell: bash - echo "--- Activation exit: $? ---" - echo "--- License dir contents: ---" - ls -la /root/.local/share/unity3d/Unity/ 2>&1 || echo "(dir not found)" + - name: Activate Unity license + run: | + unity-license-activate \ + "${{ secrets.UNITY_EMAIL }}" \ + "${{ secrets.UNITY_PASSWORD }}" \ + "${{ steps.alf.outputs.alf_path }}" + + ULF_FILE=$(find . -name "*.ulf" -o -name "*.xml" 2>/dev/null | head -1) + if [ -z "$ULF_FILE" ]; then + echo "Failed to obtain .ulf license file" + exit 1 + fi + cp "$ULF_FILE" "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" + echo "Unity license activated successfully" shell: bash + - name: Upload error screenshot + if: failure() + uses: actions/upload-artifact@v4 + with: + name: unity-license-error + path: error.png + if-no-files-found: ignore + - name: Start Unity Editor in background env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} run: | docker rm -f unity-editor 2>/dev/null || true @@ -114,12 +143,11 @@ jobs: docker run -d \ --name unity-editor \ --network host \ - -e UNITY_EMAIL \ - -e UNITY_PASSWORD \ -e UNITY_MCP_HOST=http://localhost:8080 \ -e UNITY_MCP_KEEP_CONNECTED=true \ -e UNITY_MCP_AUTH_OPTION=none \ -v "${GITHUB_WORKSPACE}:/workspace" \ + -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 07036a52d..dddb3cc42 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -68,34 +68,63 @@ jobs: done shell: bash - - name: Activate Unity license + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install unity-license-activate + run: npm install --global unity-license-activate + shell: bash + + - name: Generate Unity activation file (.alf) env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} run: | - docker rm -f unity-activate 2>/dev/null || true + mkdir -p "${GITHUB_WORKSPACE}/.unity-license" - # Run activation synchronously — exits with Unity's exit code. docker run --rm \ - --name unity-activate \ - --network host \ - -e UNITY_EMAIL \ - -e UNITY_PASSWORD \ - -v "${GITHUB_WORKSPACE}:/workspace" \ + -v "${GITHUB_WORKSPACE}/.unity-license:/output" \ + -w /output \ unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ - unity-editor -quit -batchmode -nographics -logFile /dev/stdout \ - -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" + unity-editor -batchmode -createManualActivationFile -logFile /dev/stdout || true - echo "--- Activation exit: $? ---" - echo "--- License dir contents: ---" - ls -la /root/.local/share/unity3d/Unity/ 2>&1 || echo "(dir not found)" + ALF_FILE=$(find "${GITHUB_WORKSPACE}/.unity-license" -name "*.alf" | head -1) + if [ -z "$ALF_FILE" ]; then + echo "Failed to generate .alf file" + exit 1 + fi + echo "Generated ALF file: $ALF_FILE" + echo "alf_path=${ALF_FILE}" >> "$GITHUB_OUTPUT" + id: alf shell: bash + - name: Activate Unity license + run: | + unity-license-activate \ + "${{ secrets.UNITY_EMAIL }}" \ + "${{ secrets.UNITY_PASSWORD }}" \ + "${{ steps.alf.outputs.alf_path }}" + + ULF_FILE=$(find . -name "*.ulf" -o -name "*.xml" 2>/dev/null | head -1) + if [ -z "$ULF_FILE" ]; then + echo "Failed to obtain .ulf license file" + exit 1 + fi + cp "$ULF_FILE" "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" + echo "Unity license activated successfully" + shell: bash + + - name: Upload error screenshot + if: failure() + uses: actions/upload-artifact@v4 + with: + name: unity-license-error + path: error.png + if-no-files-found: ignore + - name: Start Unity Editor in background env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} run: | docker rm -f unity-editor 2>/dev/null || true @@ -103,12 +132,11 @@ jobs: docker run -d \ --name unity-editor \ --network host \ - -e UNITY_EMAIL \ - -e UNITY_PASSWORD \ -e UNITY_MCP_HOST=http://localhost:8080 \ -e UNITY_MCP_KEEP_CONNECTED=true \ -e UNITY_MCP_AUTH_OPTION=none \ -v "${GITHUB_WORKSPACE}:/workspace" \ + -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout From 215956fe9115378f15b216ef8868d6dd442bf5aa Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 14:20:37 -0800 Subject: [PATCH 26/63] Enhance license activation process: Add logging for ULF file search and directory contents --- .github/workflows/claude.yml | 8 +++++++- .github/workflows/copilot-setup-steps.yml | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index c8c7cbbec..ceda3d20b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -117,12 +117,18 @@ jobs: "${{ secrets.UNITY_PASSWORD }}" \ "${{ steps.alf.outputs.alf_path }}" - ULF_FILE=$(find . -name "*.ulf" -o -name "*.xml" 2>/dev/null | head -1) + echo "--- Searching for .ulf file in workspace root ---" + find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" -ls 2>/dev/null + + ULF_FILE=$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" | head -1) if [ -z "$ULF_FILE" ]; then echo "Failed to obtain .ulf license file" exit 1 fi + echo "Found ULF file: $ULF_FILE" cp "$ULF_FILE" "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" + echo "--- License directory contents ---" + ls -la "${GITHUB_WORKSPACE}/.unity-license/" echo "Unity license activated successfully" shell: bash diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index dddb3cc42..92e343654 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -106,12 +106,18 @@ jobs: "${{ secrets.UNITY_PASSWORD }}" \ "${{ steps.alf.outputs.alf_path }}" - ULF_FILE=$(find . -name "*.ulf" -o -name "*.xml" 2>/dev/null | head -1) + echo "--- Searching for .ulf file in workspace root ---" + find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" -ls 2>/dev/null + + ULF_FILE=$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" | head -1) if [ -z "$ULF_FILE" ]; then echo "Failed to obtain .ulf license file" exit 1 fi + echo "Found ULF file: $ULF_FILE" cp "$ULF_FILE" "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" + echo "--- License directory contents ---" + ls -la "${GITHUB_WORKSPACE}/.unity-license/" echo "Unity license activated successfully" shell: bash From 2dcd4dc4e59eaed1f1fd47fa4ad7d597ba9cc064 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 14:32:09 -0800 Subject: [PATCH 27/63] Fix CI: allow MCP connection when UNITY_MCP_KEEP_CONNECTED=true and fix Unity readiness detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Startup.Editor.cs: respect KeepConnected override in CI — connection is now allowed when UNITY_MCP_KEEP_CONNECTED=true even if IsCi() returns true - Workflow grep pattern: replace "MCP.*ready" with "Loading completed" which matches actual Unity log output ("[Project] Loading completed in X seconds") - Timeout logs: pipe through tail -50 to avoid dumping megabytes of output Co-Authored-By: Claude Opus 4.6 --- .github/workflows/claude.yml | 4 ++-- .github/workflows/copilot-setup-steps.yml | 4 ++-- .../Assets/root/Editor/Scripts/Startup.Editor.cs | 16 ++++++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index ceda3d20b..7cba61101 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -166,13 +166,13 @@ jobs: docker logs unity-editor exit 1 fi - if docker logs unity-editor 2>&1 | grep -q "MCP.*ready\|Compilation finished\|All assemblies built"; then + if docker logs unity-editor 2>&1 | grep -q "Loading completed\|Compilation finished\|All assemblies built"; then echo "Unity is ready."; break fi echo "Still waiting... ($((i*5))s)" if [ "$i" -eq 48 ]; then echo "Timeout waiting for Unity Editor" - docker logs unity-editor + docker logs unity-editor 2>&1 | tail -50 exit 1 fi done diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 92e343654..4e22e8e81 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -155,13 +155,13 @@ jobs: docker logs unity-editor exit 1 fi - if docker logs unity-editor 2>&1 | grep -q "MCP.*ready\|Compilation finished\|All assemblies built"; then + if docker logs unity-editor 2>&1 | grep -q "Loading completed\|Compilation finished\|All assemblies built"; then echo "Unity is ready."; break fi echo "Still waiting... ($((i*5))s)" if [ "$i" -eq 48 ]; then echo "Timeout waiting for Unity Editor" - docker logs unity-editor + docker logs unity-editor 2>&1 | tail -50 exit 1 fi done diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/Startup.Editor.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/Startup.Editor.cs index b412742d0..dac56b9d1 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/Startup.Editor.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/Startup.Editor.cs @@ -104,10 +104,12 @@ static void TryDisconnectAndCleanup(string callerName, bool onlyIfConnected = fa } static void OnAfterAssemblyReload() { - var connectionAllowed = EnvironmentUtils.IsCi() == false; + var isCi = EnvironmentUtils.IsCi(); + var keepConnected = UnityMcpPluginEditor.KeepConnected; + var connectionAllowed = !isCi || keepConnected; - _logger.LogInformation("{method} triggered - BuildAndStart with connectionAllowed: {connectionAllowed}", - nameof(OnAfterAssemblyReload), connectionAllowed); + _logger.LogInformation("{method} triggered - BuildAndStart with connectionAllowed: {connectionAllowed} (isCi: {isCi}, keepConnected: {keepConnected})", + nameof(OnAfterAssemblyReload), connectionAllowed, isCi, keepConnected); UnityMcpPluginEditor.Instance.BuildMcpPluginIfNeeded(); UnityMcpPluginEditor.Instance.AddUnityLogCollectorIfNeeded(() => new BufferedFileLogStorage()); @@ -141,12 +143,14 @@ static void OnPlayModeStateChanged(PlayModeStateChange state) case PlayModeStateChange.EnteredEditMode: // Unity has returned to Edit mode - ensure connection is re-established // if the configuration expects it to be connected + var isCi = EnvironmentUtils.IsCi(); + var keepConnected = UnityMcpPluginEditor.KeepConnected; _logger.LogTrace("Entered Edit mode - KeepConnected: {keepConnected}, IsCi: {isCi}", - UnityMcpPluginEditor.KeepConnected, EnvironmentUtils.IsCi()); + keepConnected, isCi); - if (EnvironmentUtils.IsCi()) + if (isCi && !keepConnected) { - _logger.LogTrace("Skipping reconnection in CI environment"); + _logger.LogTrace("Skipping reconnection in CI environment (KeepConnected is false)"); break; } From 8a950e5f68ca6333217c727c9500f2844c1d7b30 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 14:54:26 -0800 Subject: [PATCH 28/63] Enhance CI workflows: Add UNITY_MCP_TOOLS environment variable for improved tool execution --- .github/workflows/claude.yml | 1 + .github/workflows/copilot-setup-steps.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 7cba61101..91aaf4c4c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -152,6 +152,7 @@ jobs: -e UNITY_MCP_HOST=http://localhost:8080 \ -e UNITY_MCP_KEEP_CONNECTED=true \ -e UNITY_MCP_AUTH_OPTION=none \ + -e UNITY_MCP_TOOLS=assets-refresh,console-get-logs,script-execute,tests-run \ -v "${GITHUB_WORKSPACE}:/workspace" \ -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 4e22e8e81..ba7c095e4 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -141,6 +141,7 @@ jobs: -e UNITY_MCP_HOST=http://localhost:8080 \ -e UNITY_MCP_KEEP_CONNECTED=true \ -e UNITY_MCP_AUTH_OPTION=none \ + -e UNITY_MCP_TOOLS=assets-refresh,console-get-logs,script-execute,tests-run \ -v "${GITHUB_WORKSPACE}:/workspace" \ -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ From b4a923d1682325456af8f2d7d155dfbf37c7a1f4 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 15:09:27 -0800 Subject: [PATCH 29/63] Enhance CI workflows: Integrate setup action for Unity MCP and update documentation for environment variable persistence --- .github/actions/setup-unity-mcp/action.yml | 163 ++++++++++++++++++ .github/workflows/claude.yml | 143 +-------------- .github/workflows/copilot-setup-steps.yml | 143 +-------------- README.md | 2 +- Unity-MCP-Plugin/Assets/root/README.md | 11 +- .../root/Runtime/UnityMcpPlugin.Build.cs | 3 +- docs/README.es.md | 2 +- docs/README.ja.md | 2 +- docs/README.zh-CN.md | 2 +- 9 files changed, 185 insertions(+), 286 deletions(-) create mode 100644 .github/actions/setup-unity-mcp/action.yml diff --git a/.github/actions/setup-unity-mcp/action.yml b/.github/actions/setup-unity-mcp/action.yml new file mode 100644 index 000000000..ea2eb5d0b --- /dev/null +++ b/.github/actions/setup-unity-mcp/action.yml @@ -0,0 +1,163 @@ +name: 'Setup Unity MCP' +description: 'Start MCP Server and Unity Editor in Docker containers with license activation' + +inputs: + unity-email: + description: 'Unity account email for license activation' + required: true + unity-password: + description: 'Unity account password for license activation' + required: true + unity-mcp-tools: + description: 'Comma-separated list of MCP tool IDs to enable (optional)' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Read Unity version + id: unity-version + run: | + UNITY_VERSION=$(grep "^m_EditorVersion:" Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt | awk '{print $2}') + echo "unity_version=${UNITY_VERSION}" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Cache Unity Library + uses: actions/cache@v4 + with: + path: Unity-MCP-Plugin/Library + key: unity-library-${{ steps.unity-version.outputs.unity_version }}-ubuntu-base + + - name: Start AI-Game-Developer MCP Server + run: | + docker rm -f unity-mcp-server 2>/dev/null || true + + docker run -d \ + --name unity-mcp-server \ + --network host \ + -e MCP_PLUGIN_PORT=8080 \ + -e MCP_PLUGIN_CLIENT_TRANSPORT=streamableHttp \ + -e MCP_AUTHORIZATION=none \ + ivanmurzakdev/unity-mcp-server + + echo "Waiting for MCP Server to be ready..." + for i in $(seq 1 12); do + sleep 5 + if docker logs unity-mcp-server 2>&1 | grep -q "Start listening on port:"; then + echo "MCP Server is ready."; break + fi + echo "Still waiting... ($((i*5))s)" + if [ "$i" -eq 12 ]; then + echo "Timeout waiting for MCP Server" + docker logs unity-mcp-server + exit 1 + fi + done + shell: bash + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install unity-license-activate + run: npm install --global unity-license-activate + shell: bash + + - name: Generate Unity activation file (.alf) + id: alf + env: + UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} + run: | + mkdir -p "${GITHUB_WORKSPACE}/.unity-license" + + docker run --rm \ + -v "${GITHUB_WORKSPACE}/.unity-license:/output" \ + -w /output \ + unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ + unity-editor -batchmode -createManualActivationFile -logFile /dev/stdout || true + + ALF_FILE=$(find "${GITHUB_WORKSPACE}/.unity-license" -name "*.alf" | head -1) + if [ -z "$ALF_FILE" ]; then + echo "Failed to generate .alf file" + exit 1 + fi + echo "Generated ALF file: $ALF_FILE" + echo "alf_path=${ALF_FILE}" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Activate Unity license + run: | + unity-license-activate \ + "${{ inputs.unity-email }}" \ + "${{ inputs.unity-password }}" \ + "${{ steps.alf.outputs.alf_path }}" + + echo "--- Searching for .ulf file in workspace root ---" + find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" -ls 2>/dev/null + + ULF_FILE=$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" | head -1) + if [ -z "$ULF_FILE" ]; then + echo "Failed to obtain .ulf license file" + exit 1 + fi + echo "Found ULF file: $ULF_FILE" + cp "$ULF_FILE" "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" + echo "--- License directory contents ---" + ls -la "${GITHUB_WORKSPACE}/.unity-license/" + echo "Unity license activated successfully" + shell: bash + + - name: Upload error screenshot + if: failure() + uses: actions/upload-artifact@v4 + with: + name: unity-license-error + path: error.png + if-no-files-found: ignore + + - name: Start Unity Editor in background + env: + UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} + UNITY_MCP_TOOLS: ${{ inputs.unity-mcp-tools }} + run: | + docker rm -f unity-editor 2>/dev/null || true + + TOOLS_ENV="" + if [ -n "$UNITY_MCP_TOOLS" ]; then + TOOLS_ENV="-e UNITY_MCP_TOOLS=${UNITY_MCP_TOOLS}" + fi + + docker run -d \ + --name unity-editor \ + --network host \ + -e UNITY_MCP_HOST=http://localhost:8080 \ + -e UNITY_MCP_KEEP_CONNECTED=true \ + -e UNITY_MCP_AUTH_OPTION=none \ + ${TOOLS_ENV} \ + -v "${GITHUB_WORKSPACE}:/workspace" \ + -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ + unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ + unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout + + echo "Waiting for Unity project to load..." + for i in $(seq 1 48); do + sleep 5 + # Fail fast if the container already exited + if [ "$(docker inspect -f '{{.State.Status}}' unity-editor 2>/dev/null)" = "exited" ]; then + echo "Unity Editor container exited early!" + docker logs unity-editor + exit 1 + fi + if docker logs unity-editor 2>&1 | grep -q "Loading completed\|Compilation finished\|All assemblies built"; then + echo "Unity is ready."; break + fi + echo "Still waiting... ($((i*5))s)" + if [ "$i" -eq 48 ]; then + echo "Timeout waiting for Unity Editor" + docker logs unity-editor 2>&1 | tail -50 + exit 1 + fi + done + shell: bash diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 91aaf4c4c..75a19573f 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -39,145 +39,12 @@ jobs: with: fetch-depth: 1 - - name: Read Unity version - id: unity-version - run: | - UNITY_VERSION=$(grep "^m_EditorVersion:" Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt | awk '{print $2}') - echo "unity_version=${UNITY_VERSION}" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Cache Unity Library - uses: actions/cache@v4 + - name: Setup Unity MCP + uses: ./.github/actions/setup-unity-mcp with: - path: Unity-MCP-Plugin/Library - key: unity-library-${{ steps.unity-version.outputs.unity_version }}-ubuntu-base - - - name: Start AI-Game-Developer MCP Server - run: | - docker rm -f unity-mcp-server 2>/dev/null || true - - docker run -d \ - --name unity-mcp-server \ - --network host \ - -e MCP_PLUGIN_PORT=8080 \ - -e MCP_PLUGIN_CLIENT_TRANSPORT=streamableHttp \ - -e MCP_AUTHORIZATION=none \ - ivanmurzakdev/unity-mcp-server - - echo "Waiting for MCP Server to be ready..." - for i in $(seq 1 12); do - sleep 5 - if docker logs unity-mcp-server 2>&1 | grep -q "Start listening on port:"; then - echo "MCP Server is ready."; break - fi - echo "Still waiting... ($((i*5))s)" - if [ "$i" -eq 12 ]; then - echo "Timeout waiting for MCP Server" - docker logs unity-mcp-server - exit 1 - fi - done - shell: bash - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install unity-license-activate - run: npm install --global unity-license-activate - shell: bash - - - name: Generate Unity activation file (.alf) - env: - UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} - run: | - mkdir -p "${GITHUB_WORKSPACE}/.unity-license" - - docker run --rm \ - -v "${GITHUB_WORKSPACE}/.unity-license:/output" \ - -w /output \ - unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ - unity-editor -batchmode -createManualActivationFile -logFile /dev/stdout || true - - ALF_FILE=$(find "${GITHUB_WORKSPACE}/.unity-license" -name "*.alf" | head -1) - if [ -z "$ALF_FILE" ]; then - echo "Failed to generate .alf file" - exit 1 - fi - echo "Generated ALF file: $ALF_FILE" - echo "alf_path=${ALF_FILE}" >> "$GITHUB_OUTPUT" - id: alf - shell: bash - - - name: Activate Unity license - run: | - unity-license-activate \ - "${{ secrets.UNITY_EMAIL }}" \ - "${{ secrets.UNITY_PASSWORD }}" \ - "${{ steps.alf.outputs.alf_path }}" - - echo "--- Searching for .ulf file in workspace root ---" - find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" -ls 2>/dev/null - - ULF_FILE=$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" | head -1) - if [ -z "$ULF_FILE" ]; then - echo "Failed to obtain .ulf license file" - exit 1 - fi - echo "Found ULF file: $ULF_FILE" - cp "$ULF_FILE" "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" - echo "--- License directory contents ---" - ls -la "${GITHUB_WORKSPACE}/.unity-license/" - echo "Unity license activated successfully" - shell: bash - - - name: Upload error screenshot - if: failure() - uses: actions/upload-artifact@v4 - with: - name: unity-license-error - path: error.png - if-no-files-found: ignore - - - name: Start Unity Editor in background - env: - UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} - run: | - docker rm -f unity-editor 2>/dev/null || true - - docker run -d \ - --name unity-editor \ - --network host \ - -e UNITY_MCP_HOST=http://localhost:8080 \ - -e UNITY_MCP_KEEP_CONNECTED=true \ - -e UNITY_MCP_AUTH_OPTION=none \ - -e UNITY_MCP_TOOLS=assets-refresh,console-get-logs,script-execute,tests-run \ - -v "${GITHUB_WORKSPACE}:/workspace" \ - -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ - unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ - unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout - - echo "Waiting for Unity project to load..." - for i in $(seq 1 48); do - sleep 5 - # Fail fast if the container already exited - if [ "$(docker inspect -f '{{.State.Status}}' unity-editor 2>/dev/null)" = "exited" ]; then - echo "Unity Editor container exited early!" - docker logs unity-editor - exit 1 - fi - if docker logs unity-editor 2>&1 | grep -q "Loading completed\|Compilation finished\|All assemblies built"; then - echo "Unity is ready."; break - fi - echo "Still waiting... ($((i*5))s)" - if [ "$i" -eq 48 ]; then - echo "Timeout waiting for Unity Editor" - docker logs unity-editor 2>&1 | tail -50 - exit 1 - fi - done - shell: bash + unity-email: ${{ secrets.UNITY_EMAIL }} + unity-password: ${{ secrets.UNITY_PASSWORD }} + unity-mcp-tools: 'assets-refresh,console-get-logs,script-execute,tests-run' - name: Run Claude Code id: claude diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ba7c095e4..98b817f1a 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -28,142 +28,9 @@ jobs: - name: Checkout code uses: actions/checkout@v5 - - name: Read Unity version - id: unity-version - run: | - UNITY_VERSION=$(grep "^m_EditorVersion:" Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt | awk '{print $2}') - echo "unity_version=${UNITY_VERSION}" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Cache Unity Library - uses: actions/cache@v4 - with: - path: Unity-MCP-Plugin/Library - key: unity-library-${{ steps.unity-version.outputs.unity_version }}-ubuntu-base - - - name: Start AI-Game-Developer MCP Server - run: | - docker rm -f unity-mcp-server 2>/dev/null || true - - docker run -d \ - --name unity-mcp-server \ - --network host \ - -e MCP_PLUGIN_PORT=8080 \ - -e MCP_PLUGIN_CLIENT_TRANSPORT=streamableHttp \ - -e MCP_AUTHORIZATION=none \ - ivanmurzakdev/unity-mcp-server - - echo "Waiting for MCP Server to be ready..." - for i in $(seq 1 12); do - sleep 5 - if docker logs unity-mcp-server 2>&1 | grep -q "Start listening on port:"; then - echo "MCP Server is ready."; break - fi - echo "Still waiting... ($((i*5))s)" - if [ "$i" -eq 12 ]; then - echo "Timeout waiting for MCP Server" - docker logs unity-mcp-server - exit 1 - fi - done - shell: bash - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install unity-license-activate - run: npm install --global unity-license-activate - shell: bash - - - name: Generate Unity activation file (.alf) - env: - UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} - run: | - mkdir -p "${GITHUB_WORKSPACE}/.unity-license" - - docker run --rm \ - -v "${GITHUB_WORKSPACE}/.unity-license:/output" \ - -w /output \ - unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ - unity-editor -batchmode -createManualActivationFile -logFile /dev/stdout || true - - ALF_FILE=$(find "${GITHUB_WORKSPACE}/.unity-license" -name "*.alf" | head -1) - if [ -z "$ALF_FILE" ]; then - echo "Failed to generate .alf file" - exit 1 - fi - echo "Generated ALF file: $ALF_FILE" - echo "alf_path=${ALF_FILE}" >> "$GITHUB_OUTPUT" - id: alf - shell: bash - - - name: Activate Unity license - run: | - unity-license-activate \ - "${{ secrets.UNITY_EMAIL }}" \ - "${{ secrets.UNITY_PASSWORD }}" \ - "${{ steps.alf.outputs.alf_path }}" - - echo "--- Searching for .ulf file in workspace root ---" - find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" -ls 2>/dev/null - - ULF_FILE=$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" | head -1) - if [ -z "$ULF_FILE" ]; then - echo "Failed to obtain .ulf license file" - exit 1 - fi - echo "Found ULF file: $ULF_FILE" - cp "$ULF_FILE" "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" - echo "--- License directory contents ---" - ls -la "${GITHUB_WORKSPACE}/.unity-license/" - echo "Unity license activated successfully" - shell: bash - - - name: Upload error screenshot - if: failure() - uses: actions/upload-artifact@v4 + - name: Setup Unity MCP + uses: ./.github/actions/setup-unity-mcp with: - name: unity-license-error - path: error.png - if-no-files-found: ignore - - - name: Start Unity Editor in background - env: - UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} - run: | - docker rm -f unity-editor 2>/dev/null || true - - docker run -d \ - --name unity-editor \ - --network host \ - -e UNITY_MCP_HOST=http://localhost:8080 \ - -e UNITY_MCP_KEEP_CONNECTED=true \ - -e UNITY_MCP_AUTH_OPTION=none \ - -e UNITY_MCP_TOOLS=assets-refresh,console-get-logs,script-execute,tests-run \ - -v "${GITHUB_WORKSPACE}:/workspace" \ - -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ - unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ - unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout - - echo "Waiting for Unity project to load..." - for i in $(seq 1 48); do - sleep 5 - # Fail fast if the container already exited - if [ "$(docker inspect -f '{{.State.Status}}' unity-editor 2>/dev/null)" = "exited" ]; then - echo "Unity Editor container exited early!" - docker logs unity-editor - exit 1 - fi - if docker logs unity-editor 2>&1 | grep -q "Loading completed\|Compilation finished\|All assemblies built"; then - echo "Unity is ready."; break - fi - echo "Still waiting... ($((i*5))s)" - if [ "$i" -eq 48 ]; then - echo "Timeout waiting for Unity Editor" - docker logs unity-editor 2>&1 | tail -50 - exit 1 - fi - done - shell: bash \ No newline at end of file + unity-email: ${{ secrets.UNITY_EMAIL }} + unity-password: ${{ secrets.UNITY_PASSWORD }} + unity-mcp-tools: 'assets-refresh,console-get-logs,script-execute,tests-run' diff --git a/README.md b/README.md index 3044fa121..7e8c95a72 100644 --- a/README.md +++ b/README.md @@ -506,7 +506,7 @@ Doesn't matter what launch option you choose, all of them support custom configu ## Plugin Variables -The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides **are persisted** to disk — on first run or when a new authentication token is generated, the overridden values are written to the config file and will be used in subsequent sessions. +The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are **not persisted** to disk — they only affect the current session. Each time Unity starts, the overrides must be present in the environment or command-line arguments to take effect. | Environment Variable | Command Line Arg | Values | Description | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | diff --git a/Unity-MCP-Plugin/Assets/root/README.md b/Unity-MCP-Plugin/Assets/root/README.md index 6006e5c37..7e8c95a72 100644 --- a/Unity-MCP-Plugin/Assets/root/README.md +++ b/Unity-MCP-Plugin/Assets/root/README.md @@ -506,14 +506,15 @@ Doesn't matter what launch option you choose, all of them support custom configu ## Plugin Variables -The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides **are persisted** to disk — on first run or when a new authentication token is generated, the overridden values are written to the config file and will be used in subsequent sessions. +The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are **not persisted** to disk — they only affect the current session. Each time Unity starts, the overrides must be present in the environment or command-line arguments to take effect. | Environment Variable | Command Line Arg | Values | Description | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | -| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Override the MCP Server host URL | -| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Force enable or disable the active connection | -| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Force set the authentication mode | -| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Force set the authentication token | +| `UNITY_MCP_HOST` | `-UNITY_MCP_HOST` | URL string | Override the MCP Server host URL | +| `UNITY_MCP_KEEP_CONNECTED` | `-UNITY_MCP_KEEP_CONNECTED` | `true` / `false` | Force enable or disable the active connection | +| `UNITY_MCP_AUTH_OPTION` | `-UNITY_MCP_AUTH_OPTION` | `none` / `required` | Force set the authentication mode | +| `UNITY_MCP_TOKEN` | `-UNITY_MCP_TOKEN` | string | Force set the authentication token | +| `UNITY_MCP_TOOLS` | `-UNITY_MCP_TOOLS` | comma-separated tool IDs | Enable only the listed tools; all others are disabled. Unknown IDs are logged as errors. | > Command-line args take precedence over environment variables. Both override the saved config file value. diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs index 48db9a8ca..732607948 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs @@ -180,9 +180,10 @@ protected virtual void ApplyConfigToMcpPlugin(IMcpPlugin mcpPlugin) } // Apply: enable only tools in the override list, disable all others + var enabledSet = new HashSet(enabledToolsOverride, StringComparer.OrdinalIgnoreCase); foreach (var tool in toolManager.GetAllTools()) { - var isEnabled = enabledToolsOverride.Contains(tool.Name!); + var isEnabled = enabledSet.Contains(tool.Name!); toolManager.SetToolEnabled(tool.Name!, isEnabled); _logger.LogDebug("{method}: Tool '{tool}' enabled: {isEnabled} (env override)", nameof(ApplyConfigToMcpPlugin), tool.Name, isEnabled); diff --git a/docs/README.es.md b/docs/README.es.md index b587e606e..ede07e05a 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -506,7 +506,7 @@ Sin importar qué opción de lanzamiento elijas, todas admiten configuración pe ## Variables del Plugin -El Plugin Unity MCP lee las siguientes variables de entorno (y argumentos de línea de comandos) al arrancar para sobreescribir los valores del archivo de configuración guardado. Las sobreescrituras **sí se persisten** en disco — en la primera ejecución o cuando se genera un nuevo token de autenticación, los valores sobreescritos se escriben en el archivo de configuración y se usarán en sesiones posteriores. +El Plugin Unity MCP lee las siguientes variables de entorno (y argumentos de línea de comandos) al arrancar para sobreescribir los valores del archivo de configuración guardado. Las sobreescrituras **no se persisten** en disco — solo afectan la sesión actual. Cada vez que Unity se inicia, las sobreescrituras deben estar presentes en las variables de entorno o argumentos de línea de comandos para tener efecto. | Variable de Entorno | Arg de Línea de Comandos | Valores | Descripción | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------------------- | diff --git a/docs/README.ja.md b/docs/README.ja.md index 8f6ee73a8..0b5de2f19 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -506,7 +506,7 @@ public static class ChessGameAI ## プラグイン変数 -Unity MCP Plugin は起動時に以下の環境変数(およびコマンドライン引数)を読み込み、保存済み設定ファイルの値を上書きします。上書きはディスクに**保存されます** — 初回起動時または新しい認証トークンが生成された際に、上書きされた値が設定ファイルに書き込まれ、以降のセッションでも使用されます。 +Unity MCP Plugin は起動時に以下の環境変数(およびコマンドライン引数)を読み込み、保存済み設定ファイルの値を上書きします。上書きはディスクに**保存されません** — 現在のセッションにのみ影響します。Unity を起動するたびに、上書きを有効にするには環境変数またはコマンドライン引数を指定する必要があります。 | 環境変数 | コマンドライン引数 | 値 | 説明 | | --------------------------- | --------------------------- | ------------------- | ---------------------------------------- | diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index c4beade68..8f9b072aa 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -506,7 +506,7 @@ public static class ChessGameAI ## 插件变量 -Unity MCP 插件在启动时读取以下环境变量(及命令行参数),用于覆盖已保存配置文件中的值。覆盖**会持久化**到磁盘 — 在首次运行或生成新认证令牌时,被覆盖的值将写入配置文件,并在后续会话中继续生效。 +Unity MCP 插件在启动时读取以下环境变量(及命令行参数),用于覆盖已保存配置文件中的值。覆盖**不会持久化**到磁盘 — 仅影响当前会话。每次启动 Unity 时,必须在环境变量或命令行参数中提供覆盖值才能生效。 | 环境变量 | 命令行参数 | 值 | 描述 | | --------------------------- | --------------------------- | ------------------- | ------------------------------ | From 9fb1ee7060ef97591769a813badcf760e1510a18 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 16:10:25 -0800 Subject: [PATCH 30/63] Enhance workflows: Add author and license information to workflow files and improve formatting consistency --- .github/actions/setup-unity-mcp/action.yml | 45 +++++++++++----- .github/workflows/bump_version.yml | 8 +++ .github/workflows/claude.yml | 10 +++- .github/workflows/copilot-setup-steps.yml | 10 +++- .github/workflows/deploy.yml | 20 ++++--- .../workflows/deploy_server_executables.yml | 52 +++++++++++-------- .github/workflows/release.yml | 16 +++++- .github/workflows/test_pull_request.yml | 8 +++ .../workflows/test_pull_request_manual.yml | 8 +++ .github/workflows/test_unity_plugin.yml | 38 ++++++++------ 10 files changed, 157 insertions(+), 58 deletions(-) diff --git a/.github/actions/setup-unity-mcp/action.yml b/.github/actions/setup-unity-mcp/action.yml index ea2eb5d0b..8aacdbcd2 100644 --- a/.github/actions/setup-unity-mcp/action.yml +++ b/.github/actions/setup-unity-mcp/action.yml @@ -1,20 +1,28 @@ -name: 'Setup Unity MCP' -description: 'Start MCP Server and Unity Editor in Docker containers with license activation' +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + +name: "Setup Unity MCP" +description: "Start MCP Server and Unity Editor in Docker containers with license activation" inputs: unity-email: - description: 'Unity account email for license activation' + description: "Unity account email for license activation" required: true unity-password: - description: 'Unity account password for license activation' + description: "Unity account password for license activation" required: true unity-mcp-tools: - description: 'Comma-separated list of MCP tool IDs to enable (optional)' + description: "Comma-separated list of MCP tool IDs to enable (optional)" required: false - default: '' + default: "" runs: - using: 'composite' + using: "composite" steps: - name: Read Unity version id: unity-version @@ -59,7 +67,7 @@ runs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Install unity-license-activate run: npm install --global unity-license-activate @@ -89,10 +97,23 @@ runs: - name: Activate Unity license run: | - unity-license-activate \ - "${{ inputs.unity-email }}" \ - "${{ inputs.unity-password }}" \ - "${{ steps.alf.outputs.alf_path }}" + MAX_RETRIES=5 + for attempt in $(seq 1 $MAX_RETRIES); do + echo "=== License activation attempt $attempt of $MAX_RETRIES ===" + if unity-license-activate \ + "${{ inputs.unity-email }}" \ + "${{ inputs.unity-password }}" \ + "${{ steps.alf.outputs.alf_path }}"; then + echo "unity-license-activate succeeded on attempt $attempt" + break + fi + if [ "$attempt" -eq "$MAX_RETRIES" ]; then + echo "All $MAX_RETRIES license activation attempts failed" + exit 1 + fi + echo "Attempt $attempt failed, retrying in 10s..." + sleep 10 + done echo "--- Searching for .ulf file in workspace root ---" find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" -ls 2>/dev/null diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 1dd095bf1..ae4cb5c26 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + name: bump version on: diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 75a19573f..bc85ae7bc 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + name: Claude Code on: @@ -44,7 +52,7 @@ jobs: with: unity-email: ${{ secrets.UNITY_EMAIL }} unity-password: ${{ secrets.UNITY_PASSWORD }} - unity-mcp-tools: 'assets-refresh,console-get-logs,script-execute,tests-run' + unity-mcp-tools: "assets-refresh,console-get-logs,script-execute,tests-run" - name: Run Claude Code id: claude diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 98b817f1a..b7944b248 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + name: "Copilot Setup Steps" # Automatically run the setup steps when they are changed to allow for easy validation, and @@ -33,4 +41,4 @@ jobs: with: unity-email: ${{ secrets.UNITY_EMAIL }} unity-password: ${{ secrets.UNITY_PASSWORD }} - unity-mcp-tools: 'assets-refresh,console-get-logs,script-execute,tests-run' + unity-mcp-tools: "assets-refresh,console-get-logs,script-execute,tests-run" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a210162ac..fe3d2a165 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + name: deploy on: @@ -7,7 +15,7 @@ on: workflow_dispatch: inputs: version: - description: 'Deploy to NuGet and Docker Hub version (e.g., 1.0.0).' + description: "Deploy to NuGet and Docker Hub version (e.g., 1.0.0)." required: true type: string @@ -15,9 +23,9 @@ on: inputs: version: { required: true, type: string } secrets: - NUGET_API_KEY: { required: true } - DOCKER_USERNAME: { required: true } - DOCKER_PASSWORD: { required: true } + NUGET_API_KEY: { required: true } + DOCKER_USERNAME: { required: true } + DOCKER_PASSWORD: { required: true } jobs: deploy-mcp-server-to-nuget: @@ -36,7 +44,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: "9.0.x" - name: Restore dependencies run: | @@ -102,4 +110,4 @@ jobs: ivanmurzakdev/unity-mcp-server:latest platforms: linux/amd64,linux/arm64 cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file + cache-to: type=gha,mode=max diff --git a/.github/workflows/deploy_server_executables.yml b/.github/workflows/deploy_server_executables.yml index ea4731442..f1ab0920b 100644 --- a/.github/workflows/deploy_server_executables.yml +++ b/.github/workflows/deploy_server_executables.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + # This workflow builds the MCP server executables for all platforms and uploads them as release assets. # It is triggered when a new release is published manually. # It is NOT triggered by CI/CD pipelines or other automated processes. @@ -12,25 +20,25 @@ jobs: build-and-zip: runs-on: macos-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: Make build script executable - run: chmod +x ./Unity-MCP-Server/build-all.sh - - - name: Build executables for all platforms - shell: bash {0} - run: cd Unity-MCP-Server && ./build-all.sh Release - - - name: Upload release assets - uses: softprops/action-gh-release@v2 - with: - files: ./Unity-MCP-Server/publish/*.zip - tag_name: ${{ github.event.release.tag_name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "9.0.x" + + - name: Make build script executable + run: chmod +x ./Unity-MCP-Server/build-all.sh + + - name: Build executables for all platforms + shell: bash {0} + run: cd Unity-MCP-Server && ./build-all.sh Release + + - name: Upload release assets + uses: softprops/action-gh-release@v2 + with: + files: ./Unity-MCP-Server/publish/*.zip + tag_name: ${{ github.event.release.tag_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df598406c..b8b7a769a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + name: release on: @@ -338,7 +346,13 @@ jobs: publish_discord: runs-on: ubuntu-latest - needs: [release-unity-plugin, publish-unity-installer, publish-mcp-server, deploy] + needs: + [ + release-unity-plugin, + publish-unity-installer, + publish-mcp-server, + deploy, + ] if: | always() && needs.release-unity-plugin.result == 'success' && diff --git a/.github/workflows/test_pull_request.yml b/.github/workflows/test_pull_request.yml index 0a718259d..9701ba308 100644 --- a/.github/workflows/test_pull_request.yml +++ b/.github/workflows/test_pull_request.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + name: test-pull-request on: diff --git a/.github/workflows/test_pull_request_manual.yml b/.github/workflows/test_pull_request_manual.yml index bd452c1ed..5a67a78b8 100644 --- a/.github/workflows/test_pull_request_manual.yml +++ b/.github/workflows/test_pull_request_manual.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + name: test-pull-request on: diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index 775533fe4..952601648 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -1,3 +1,11 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + name: test-unity-plugin ############################################################################## @@ -6,12 +14,12 @@ name: test-unity-plugin on: workflow_call: inputs: - projectPath: { required: true, type: string } - unityVersion: { required: true, type: string } - testMode: { required: true, type: string } + projectPath: { required: true, type: string } + unityVersion: { required: true, type: string } + testMode: { required: true, type: string } secrets: - UNITY_LICENSE: { required: true } - UNITY_EMAIL: { required: true } + UNITY_LICENSE: { required: true } + UNITY_EMAIL: { required: true } UNITY_PASSWORD: { required: true } ############################################################################## @@ -81,17 +89,17 @@ jobs: - uses: game-ci/unity-test-runner@v4 id: tests env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: - projectPath: ${{ inputs.projectPath }} - unityVersion: ${{ inputs.unityVersion }} - testMode: ${{ inputs.testMode }} - customImage: ${{ steps.custom_image.outputs.image }} - githubToken: ${{ secrets.GITHUB_TOKEN }} - checkName: ${{ inputs.unityVersion }} ${{ inputs.testMode }} ${{ matrix.platform }} Test Results - artifactsPath: artifacts-${{ inputs.unityVersion }}-${{ inputs.testMode }}-${{ matrix.platform }} + projectPath: ${{ inputs.projectPath }} + unityVersion: ${{ inputs.unityVersion }} + testMode: ${{ inputs.testMode }} + customImage: ${{ steps.custom_image.outputs.image }} + githubToken: ${{ secrets.GITHUB_TOKEN }} + checkName: ${{ inputs.unityVersion }} ${{ inputs.testMode }} ${{ matrix.platform }} Test Results + artifactsPath: artifacts-${{ inputs.unityVersion }}-${{ inputs.testMode }}-${{ matrix.platform }} customParameters: -CI true -GITHUB_ACTIONS true # --------------------------------------------------------------------- # @@ -99,4 +107,4 @@ jobs: if: always() with: name: Test results for ${{ inputs.unityVersion }} ${{ inputs.testMode }} on ${{ matrix.platform }} - path: ${{ steps.tests.outputs.artifactsPath }} \ No newline at end of file + path: ${{ steps.tests.outputs.artifactsPath }} From 99e3493aace1595f9687e389e5d80e23bbac6a82 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 16:23:04 -0800 Subject: [PATCH 31/63] Enhance setup action: Add inputs for project path and caching options for improved flexibility --- .github/actions/setup-unity-mcp/action.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-unity-mcp/action.yml b/.github/actions/setup-unity-mcp/action.yml index 8aacdbcd2..e5f919cfb 100644 --- a/.github/actions/setup-unity-mcp/action.yml +++ b/.github/actions/setup-unity-mcp/action.yml @@ -16,6 +16,14 @@ inputs: unity-password: description: "Unity account password for license activation" required: true + unity-project-path: + description: "Relative path to the Unity project folder" + required: false + default: "Unity-MCP-Plugin" + cache-library: + description: "Enable caching of the Unity Library folder" + required: false + default: "true" unity-mcp-tools: description: "Comma-separated list of MCP tool IDs to enable (optional)" required: false @@ -27,14 +35,15 @@ runs: - name: Read Unity version id: unity-version run: | - UNITY_VERSION=$(grep "^m_EditorVersion:" Unity-MCP-Plugin/ProjectSettings/ProjectVersion.txt | awk '{print $2}') + UNITY_VERSION=$(grep "^m_EditorVersion:" ${{ inputs.unity-project-path }}/ProjectSettings/ProjectVersion.txt | awk '{print $2}') echo "unity_version=${UNITY_VERSION}" >> "$GITHUB_OUTPUT" shell: bash - name: Cache Unity Library + if: inputs.cache-library == 'true' uses: actions/cache@v4 with: - path: Unity-MCP-Plugin/Library + path: ${{ inputs.unity-project-path }}/Library key: unity-library-${{ steps.unity-version.outputs.unity_version }}-ubuntu-base - name: Start AI-Game-Developer MCP Server @@ -160,7 +169,7 @@ runs: -v "${GITHUB_WORKSPACE}:/workspace" \ -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ - unity-editor -batchmode -projectPath /workspace/Unity-MCP-Plugin -logFile /dev/stdout + unity-editor -batchmode -projectPath /workspace/${{ inputs.unity-project-path }} -logFile /dev/stdout echo "Waiting for Unity project to load..." for i in $(seq 1 48); do From 4c61f0b1d40e8bcf66e026d0bacfb4c5ebdd6122 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 16:29:20 -0800 Subject: [PATCH 32/63] Enhance workflows: Use secret for UNITY_MCP_TOOLS in workflow files for improved flexibility --- .github/workflows/claude.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index bc85ae7bc..329913186 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -52,7 +52,7 @@ jobs: with: unity-email: ${{ secrets.UNITY_EMAIL }} unity-password: ${{ secrets.UNITY_PASSWORD }} - unity-mcp-tools: "assets-refresh,console-get-logs,script-execute,tests-run" + unity-mcp-tools: ${{ secrets.UNITY_MCP_TOOLS || 'assets-refresh,console-get-logs,script-execute,tests-run' }} - name: Run Claude Code id: claude diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index b7944b248..59feea273 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -41,4 +41,4 @@ jobs: with: unity-email: ${{ secrets.UNITY_EMAIL }} unity-password: ${{ secrets.UNITY_PASSWORD }} - unity-mcp-tools: "assets-refresh,console-get-logs,script-execute,tests-run" + unity-mcp-tools: ${{ secrets.UNITY_MCP_TOOLS || 'assets-refresh,console-get-logs,script-execute,tests-run' }} From e798bbb533ad8bc0e62e7c7110839486db9ba791 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 16:52:05 -0800 Subject: [PATCH 33/63] Enhance workflows: Upgrade actions/checkout from v4 to v5 for improved performance and compatibility --- .github/actions/setup-unity-mcp/action.yml | 1 + .github/workflows/bump_version.yml | 2 +- .github/workflows/claude.yml | 2 +- .github/workflows/deploy.yml | 4 ++-- .../workflows/deploy_server_executables.yml | 2 +- .github/workflows/release.yml | 10 +++++----- .github/workflows/test_pull_request.yml | 2 +- .github/workflows/test_unity_plugin.yml | 2 +- README.md | 2 +- .../Editor/Scripts/UnityMcpPluginEditor.cs | 2 +- Unity-MCP-Plugin/Assets/root/README.md | 2 +- .../root/Runtime/Utils/EnvironmentUtils.cs | 18 ++++++++++++++---- docs/README.es.md | 2 +- docs/README.ja.md | 2 +- docs/README.zh-CN.md | 2 +- 15 files changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/actions/setup-unity-mcp/action.yml b/.github/actions/setup-unity-mcp/action.yml index e5f919cfb..59f490167 100644 --- a/.github/actions/setup-unity-mcp/action.yml +++ b/.github/actions/setup-unity-mcp/action.yml @@ -91,6 +91,7 @@ runs: docker run --rm \ -v "${GITHUB_WORKSPACE}/.unity-license:/output" \ + -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ -w /output \ unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ unity-editor -batchmode -createManualActivationFile -logFile /dev/stdout || true diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index ae4cb5c26..19da8790d 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -28,7 +28,7 @@ jobs: fi - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 329913186..4fa555bdb 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -43,7 +43,7 @@ jobs: 9.0.x - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fe3d2a165..bf0b3bcdd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Extract version from tag id: version @@ -82,7 +82,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Extract version from tag id: version diff --git a/.github/workflows/deploy_server_executables.yml b/.github/workflows/deploy_server_executables.yml index f1ab0920b..4ad23d85f 100644 --- a/.github/workflows/deploy_server_executables.yml +++ b/.github/workflows/deploy_server_executables.yml @@ -21,7 +21,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup .NET uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8b7a769a..5c2d3595a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: tag_exists: ${{ steps.tag_exists.outputs.exists }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: true @@ -49,7 +49,7 @@ jobs: if: needs.check-version-tag.outputs.tag_exists == 'false' steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Cache Unity Library uses: actions/cache@v4 @@ -111,7 +111,7 @@ jobs: if: needs.check-version-tag.outputs.tag_exists == 'false' steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -246,7 +246,7 @@ jobs: release_notes: ${{ steps.rel_desc.outputs.release_body }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: true @@ -328,7 +328,7 @@ jobs: if: needs.release-unity-plugin.outputs.success == 'true' steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download build zips artifact uses: actions/download-artifact@v4 diff --git a/.github/workflows/test_pull_request.yml b/.github/workflows/test_pull_request.yml index 9701ba308..a59f72894 100644 --- a/.github/workflows/test_pull_request.yml +++ b/.github/workflows/test_pull_request.yml @@ -19,7 +19,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup .NET uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index 952601648..54f153ba7 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -58,7 +58,7 @@ jobs: # --------------------------------------------------------------------- # # 2-b. Checkout the contributor’s commit safely # --------------------------------------------------------------------- # - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: lfs: false diff --git a/README.md b/README.md index 7e8c95a72..1770bd351 100644 --- a/README.md +++ b/README.md @@ -506,7 +506,7 @@ Doesn't matter what launch option you choose, all of them support custom configu ## Plugin Variables -The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are **not persisted** to disk — they only affect the current session. Each time Unity starts, the overrides must be present in the environment or command-line arguments to take effect. +The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are applied at runtime; on first run or when a new authentication token is generated, the overridden values are **written to the config file**. On subsequent runs, overrides are applied in memory but are not automatically saved. The exception is `UNITY_MCP_TOOLS`, which uses `[JsonIgnore]` and is **never persisted** — it is runtime-only. | Environment Variable | Command Line Arg | Values | Description | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs index 15531c41a..5df193f99 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.cs @@ -25,9 +25,9 @@ protected UnityMcpPluginEditor() : base() var config = GetOrCreateConfig(out var wasCreated); unityConnectionConfig = config; ApplyLogLevel(unityConnectionConfig.LogLevel); + EnvironmentUtils.ApplyEnvironmentOverrides(unityConnectionConfig); if (wasCreated) Save(); - EnvironmentUtils.ApplyEnvironmentOverrides(unityConnectionConfig); IncrementSingletonCount(); } diff --git a/Unity-MCP-Plugin/Assets/root/README.md b/Unity-MCP-Plugin/Assets/root/README.md index 7e8c95a72..1770bd351 100644 --- a/Unity-MCP-Plugin/Assets/root/README.md +++ b/Unity-MCP-Plugin/Assets/root/README.md @@ -506,7 +506,7 @@ Doesn't matter what launch option you choose, all of them support custom configu ## Plugin Variables -The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are **not persisted** to disk — they only affect the current session. Each time Unity starts, the overrides must be present in the environment or command-line arguments to take effect. +The Unity MCP Plugin reads the following environment variables (and command-line arguments) on startup to override values from the saved config file. Overrides are applied at runtime; on first run or when a new authentication token is generated, the overridden values are **written to the config file**. On subsequent runs, overrides are applied in memory but are not automatically saved. The exception is `UNITY_MCP_TOOLS`, which uses `[JsonIgnore]` and is **never persisted** — it is runtime-only. | Environment Variable | Command Line Arg | Values | Description | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------- | diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs index 839aa5508..accd97b4f 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/Utils/EnvironmentUtils.cs @@ -22,8 +22,10 @@ public static class EnvironmentUtils { static readonly ILogger _logger = UnityLoggerFactory.LoggerFactory.CreateLogger(nameof(EnvironmentUtils)); // Environment variable names for MCP connection overrides. - // These override values loaded from the JSON config file. Overrides are applied after - // any initial Save() so they are never written back to disk. + // These override values loaded from the JSON config file. On first run (when the config + // is newly created), overrides are applied before Save() so they are persisted to disk. + // On subsequent runs the config already exists, so overrides are applied at runtime only. + // Exception: UNITY_MCP_TOOLS uses [JsonIgnore] and is never persisted. public const string EnvHost = "UNITY_MCP_HOST"; public const string EnvKeepConnected = "UNITY_MCP_KEEP_CONNECTED"; public const string EnvAuthOption = "UNITY_MCP_AUTH_OPTION"; @@ -50,7 +52,10 @@ public static bool IsCi() /// Applies environment variable (or command-line argument) overrides to the given config. /// Checks command-line args first, then falls back to process environment variables. /// Invalid or missing values are silently ignored, leaving the config field unchanged. - /// Overrides are applied after any initial config save and are never written back to disk. + /// On first run (when the config is newly created), overrides are applied before Save() + /// so they are persisted to disk. On subsequent runs they act as runtime-only overrides. + /// Exception: (UNITY_MCP_TOOLS) targets a + /// [JsonIgnore] property and is never persisted regardless of timing. /// public static void ApplyEnvironmentOverrides(UnityMcpPlugin.UnityConnectionConfig config) { @@ -95,9 +100,14 @@ public static void ApplyEnvironmentOverrides(UnityMcpPlugin.UnityConnectionConfi if (!string.IsNullOrWhiteSpace(tools)) { var ids = tools.Split(',', StringSplitOptions.RemoveEmptyEntries); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); var trimmed = new List(ids.Length); foreach (var id in ids) - trimmed.Add(id.Trim().Trim('"')); + { + var value = id.Trim().Trim('"'); + if (!string.IsNullOrWhiteSpace(value) && seen.Add(value)) + trimmed.Add(value); + } config.EnabledToolsOverride = trimmed; _logger.LogInformation("[MCP] Env override: {Key}={Value}", EnvTools, tools.Trim()); } diff --git a/docs/README.es.md b/docs/README.es.md index ede07e05a..4a5347ca4 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -506,7 +506,7 @@ Sin importar qué opción de lanzamiento elijas, todas admiten configuración pe ## Variables del Plugin -El Plugin Unity MCP lee las siguientes variables de entorno (y argumentos de línea de comandos) al arrancar para sobreescribir los valores del archivo de configuración guardado. Las sobreescrituras **no se persisten** en disco — solo afectan la sesión actual. Cada vez que Unity se inicia, las sobreescrituras deben estar presentes en las variables de entorno o argumentos de línea de comandos para tener efecto. +El Plugin Unity MCP lee las siguientes variables de entorno (y argumentos de línea de comandos) al arrancar para sobreescribir los valores del archivo de configuración guardado. Las sobreescrituras se aplican en tiempo de ejecución; en la primera ejecución o cuando se genera un nuevo token de autenticación, los valores sobreescritos se **escriben en el archivo de configuración**. En ejecuciones posteriores, las sobreescrituras se aplican en memoria pero no se guardan automáticamente. La excepción es `UNITY_MCP_TOOLS`, que usa `[JsonIgnore]` y **nunca se persiste** — solo funciona en tiempo de ejecución. | Variable de Entorno | Arg de Línea de Comandos | Valores | Descripción | | --------------------------- | --------------------------- | ------------------- | --------------------------------------------------------- | diff --git a/docs/README.ja.md b/docs/README.ja.md index 0b5de2f19..a932c34b5 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -506,7 +506,7 @@ public static class ChessGameAI ## プラグイン変数 -Unity MCP Plugin は起動時に以下の環境変数(およびコマンドライン引数)を読み込み、保存済み設定ファイルの値を上書きします。上書きはディスクに**保存されません** — 現在のセッションにのみ影響します。Unity を起動するたびに、上書きを有効にするには環境変数またはコマンドライン引数を指定する必要があります。 +Unity MCP Plugin は起動時に以下の環境変数(およびコマンドライン引数)を読み込み、保存済み設定ファイルの値を上書きします。上書きはランタイムで適用されます。初回起動時または新しい認証トークンが生成された際には、上書きされた値が**設定ファイルに書き込まれます**。以降の起動では、上書きはメモリ上でのみ適用され、自動的には保存されません。例外として `UNITY_MCP_TOOLS` は `[JsonIgnore]` を使用しており、**永続化されません** — ランタイムのみで有効です。 | 環境変数 | コマンドライン引数 | 値 | 説明 | | --------------------------- | --------------------------- | ------------------- | ---------------------------------------- | diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 8f9b072aa..1324d25c4 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -506,7 +506,7 @@ public static class ChessGameAI ## 插件变量 -Unity MCP 插件在启动时读取以下环境变量(及命令行参数),用于覆盖已保存配置文件中的值。覆盖**不会持久化**到磁盘 — 仅影响当前会话。每次启动 Unity 时,必须在环境变量或命令行参数中提供覆盖值才能生效。 +Unity MCP 插件在启动时读取以下环境变量(及命令行参数),用于覆盖已保存配置文件中的值。覆盖在运行时生效;在首次运行或生成新的认证令牌时,覆盖值会被**写入配置文件**。在后续运行中,覆盖仅在内存中生效,不会自动保存。例外情况是 `UNITY_MCP_TOOLS`,它使用 `[JsonIgnore]` 且**永远不会持久化** — 仅在运行时生效。 | 环境变量 | 命令行参数 | 值 | 描述 | | --------------------------- | --------------------------- | ------------------- | ------------------------------ | From 6ee8dd2284d8b96aa3e5c2b0d7b94e9465c79a47 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 17:00:03 -0800 Subject: [PATCH 34/63] Enhance setup action: Update Docker image tag for unity-mcp-server to latest for improved stability --- .github/actions/setup-unity-mcp/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-unity-mcp/action.yml b/.github/actions/setup-unity-mcp/action.yml index 59f490167..d0d48571a 100644 --- a/.github/actions/setup-unity-mcp/action.yml +++ b/.github/actions/setup-unity-mcp/action.yml @@ -56,7 +56,7 @@ runs: -e MCP_PLUGIN_PORT=8080 \ -e MCP_PLUGIN_CLIENT_TRANSPORT=streamableHttp \ -e MCP_AUTHORIZATION=none \ - ivanmurzakdev/unity-mcp-server + ivanmurzakdev/unity-mcp-server:latest echo "Waiting for MCP Server to be ready..." for i in $(seq 1 12); do From 5a8aadd5e477e776990ceca5e2ef0404afc50769 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 18:21:54 -0800 Subject: [PATCH 35/63] Enhance workflows: Update unity-mcp-tools logic for better handling of empty secrets and rename test-pull-request to test-pull-request-manual --- .github/workflows/claude.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/test_pull_request_manual.yml | 2 +- Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 4fa555bdb..b4de06576 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -52,7 +52,7 @@ jobs: with: unity-email: ${{ secrets.UNITY_EMAIL }} unity-password: ${{ secrets.UNITY_PASSWORD }} - unity-mcp-tools: ${{ secrets.UNITY_MCP_TOOLS || 'assets-refresh,console-get-logs,script-execute,tests-run' }} + unity-mcp-tools: ${{ secrets.UNITY_MCP_TOOLS != '' && secrets.UNITY_MCP_TOOLS || 'assets-refresh,console-get-logs,script-execute,tests-run' }} - name: Run Claude Code id: claude diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 59feea273..afa699164 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -41,4 +41,4 @@ jobs: with: unity-email: ${{ secrets.UNITY_EMAIL }} unity-password: ${{ secrets.UNITY_PASSWORD }} - unity-mcp-tools: ${{ secrets.UNITY_MCP_TOOLS || 'assets-refresh,console-get-logs,script-execute,tests-run' }} + unity-mcp-tools: ${{ secrets.UNITY_MCP_TOOLS != '' && secrets.UNITY_MCP_TOOLS || 'assets-refresh,console-get-logs,script-execute,tests-run' }} diff --git a/.github/workflows/test_pull_request_manual.yml b/.github/workflows/test_pull_request_manual.yml index 5a67a78b8..c4b6cab53 100644 --- a/.github/workflows/test_pull_request_manual.yml +++ b/.github/workflows/test_pull_request_manual.yml @@ -6,7 +6,7 @@ # │ See the LICENSE file in the project root for more information. │ # └──────────────────────────────────────────────────────────────────┘ -name: test-pull-request +name: test-pull-request-manual on: workflow_dispatch: diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs index 732607948..fc64cefbb 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs @@ -15,6 +15,7 @@ using com.IvanMurzak.McpPlugin; using com.IvanMurzak.ReflectorNet; using com.IvanMurzak.ReflectorNet.Utils; +using com.IvanMurzak.Unity.MCP.Runtime.Utils; using com.IvanMurzak.Unity.MCP.Utils; using Microsoft.Extensions.Logging; using R3; @@ -176,7 +177,7 @@ protected virtual void ApplyConfigToMcpPlugin(IMcpPlugin mcpPlugin) { if (!allToolNames.Contains(requestedId)) _logger.LogError("[MCP] {Key}: tool '{ToolId}' not found. Check the tool ID.", - "UNITY_MCP_TOOLS", requestedId); + EnvironmentUtils.EnvTools, requestedId); } // Apply: enable only tools in the override list, disable all others From de4f5879ccc467f4df85c7e2d4c10a51de07b3c5 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 18:45:24 -0800 Subject: [PATCH 36/63] Refactor tool management: Optimize tool validation and enablement logic by caching tool list --- .../root/Editor/Scripts/UnityMcpPluginEditor.Config.cs | 1 - .../Assets/root/Runtime/UnityMcpPlugin.Build.cs | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs index 396b33e93..dfb326bc9 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UnityMcpPluginEditor.Config.cs @@ -13,7 +13,6 @@ using System.IO; using System.Linq; using System.Text.Json; -using com.IvanMurzak.Unity.MCP.Runtime.Utils; using Microsoft.Extensions.Logging; using UnityEngine; diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs index fc64cefbb..8acfa35b1 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.Build.cs @@ -169,9 +169,12 @@ protected virtual void ApplyConfigToMcpPlugin(IMcpPlugin mcpPlugin) var enabledToolsOverride = unityConnectionConfig.EnabledToolsOverride; if (enabledToolsOverride != null) { + var allTools = toolManager.GetAllTools().ToList(); + var enabledSet = new HashSet(enabledToolsOverride, StringComparer.OrdinalIgnoreCase); + // Validate requested tool IDs against the registered tool list var allToolNames = new HashSet( - toolManager.GetAllTools().Select(t => t.Name!), + allTools.Select(t => t.Name!), StringComparer.OrdinalIgnoreCase); foreach (var requestedId in enabledToolsOverride) { @@ -181,8 +184,7 @@ protected virtual void ApplyConfigToMcpPlugin(IMcpPlugin mcpPlugin) } // Apply: enable only tools in the override list, disable all others - var enabledSet = new HashSet(enabledToolsOverride, StringComparer.OrdinalIgnoreCase); - foreach (var tool in toolManager.GetAllTools()) + foreach (var tool in allTools) { var isEnabled = enabledSet.Contains(tool.Name!); toolManager.SetToolEnabled(tool.Name!, isEnabled); From 3f439d30b91a9b63c80c503f748811671fdbda47 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 20:59:47 -0800 Subject: [PATCH 37/63] Update Claude workflow permissions to allow write access for contents, pull requests, and issues --- .github/workflows/claude.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index b4de06576..667f8027e 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -29,9 +29,9 @@ jobs: (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest permissions: - contents: read - pull-requests: read - issues: read + contents: write + pull-requests: write + issues: write id-token: write actions: read # Required for Claude to read CI results on PRs steps: From 20bcd5e6d85f3b47a2878189ce6ebe373202f7a4 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 21:00:49 -0800 Subject: [PATCH 38/63] Update .gitignore: Add .unity-license and ensure newline at end of file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 19a39ecbc..9a10f7823 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # GitHub .secrets +.unity-license # AI configurations .claude @@ -16,4 +17,4 @@ commands/.env .CONVENTIONS.md # Wiki project -Unity-MCP.wiki \ No newline at end of file +Unity-MCP.wiki From e8d75de226aad12968b9ee4e336f6fa71516936e Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 23:08:05 -0800 Subject: [PATCH 39/63] Refactor Claude workflow: Simplify allowedTools syntax and enable full output display --- .github/workflows/claude.yml | 43 +++++++++++++++++------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 667f8027e..c218513d8 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -65,31 +65,28 @@ jobs: additional_permissions: | actions: read - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' + show_full_output: true - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options claude_args: >- - --allowedTools Bash(*) - --allowedTools Read(*) - --allowedTools Write(*) - --allowedTools Edit(*) - --allowedTools Glob(*) - --allowedTools Grep(*) - --allowedTools WebFetch(*) - --allowedTools WebSearch(*) - --allowedTools Agent(*) - --allowedTools NotebookEdit(*) - --allowedTools TodoWrite - --allowedTools Skill(*) - --allowedTools TaskOutput(*) - --allowedTools TaskStop(*) - --allowedTools EnterPlanMode - --allowedTools ExitPlanMode - --allowedTools EnterWorktree - --allowedTools AskUserQuestion + --allowedTools "Bash(*)" + --allowedTools "Read(*)" + --allowedTools "Write(*)" + --allowedTools "Edit(*)" + --allowedTools "Glob(*)" + --allowedTools "Grep(*)" + --allowedTools "WebFetch(*)" + --allowedTools "WebSearch(*)" + --allowedTools "Agent(*)" + --allowedTools "NotebookEdit(*)" + --allowedTools "TodoWrite" + --allowedTools "Skill(*)" + --allowedTools "TaskOutput(*)" + --allowedTools "TaskStop(*)" + --allowedTools "EnterPlanMode" + --allowedTools "ExitPlanMode" + --allowedTools "EnterWorktree" + --allowedTools "AskUserQuestion" + --allowedTools "mcp__ai-game-developer__*" --mcp-config '{"mcpServers":{"ai-game-developer":{"type":"streamableHttp","url":"http://localhost:8080"}}}' - name: Cleanup Docker containers From fdd1beaf2f5496bb913d9aada5c6c77a363b98db Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 6 Mar 2026 23:46:30 -0800 Subject: [PATCH 40/63] Add mcp-servers configuration and update Claude workflow to use it --- .github/configs/mcp-servers.json | 8 ++++++ .github/workflows/claude.yml | 48 +++++++++++++++++++------------- 2 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 .github/configs/mcp-servers.json diff --git a/.github/configs/mcp-servers.json b/.github/configs/mcp-servers.json new file mode 100644 index 000000000..b61c254e8 --- /dev/null +++ b/.github/configs/mcp-servers.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "ai-game-developer": { + "type": "streamableHttp", + "url": "http://localhost:8080" + } + } +} diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index c218513d8..c8f48fbbf 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -67,27 +67,35 @@ jobs: show_full_output: true + settings: | + { + "permissions": { + "allow": [ + "Bash(*)", + "Read(*)", + "Write(*)", + "Edit(*)", + "Glob(*)", + "Grep(*)", + "WebFetch(*)", + "WebSearch(*)", + "Agent(*)", + "NotebookEdit(*)", + "TodoWrite", + "Skill(*)", + "TaskOutput(*)", + "TaskStop(*)", + "EnterPlanMode", + "ExitPlanMode", + "EnterWorktree", + "AskUserQuestion", + "mcp__ai-game-developer__*" + ] + } + } + claude_args: >- - --allowedTools "Bash(*)" - --allowedTools "Read(*)" - --allowedTools "Write(*)" - --allowedTools "Edit(*)" - --allowedTools "Glob(*)" - --allowedTools "Grep(*)" - --allowedTools "WebFetch(*)" - --allowedTools "WebSearch(*)" - --allowedTools "Agent(*)" - --allowedTools "NotebookEdit(*)" - --allowedTools "TodoWrite" - --allowedTools "Skill(*)" - --allowedTools "TaskOutput(*)" - --allowedTools "TaskStop(*)" - --allowedTools "EnterPlanMode" - --allowedTools "ExitPlanMode" - --allowedTools "EnterWorktree" - --allowedTools "AskUserQuestion" - --allowedTools "mcp__ai-game-developer__*" - --mcp-config '{"mcpServers":{"ai-game-developer":{"type":"streamableHttp","url":"http://localhost:8080"}}}' + --mcp-config .github/configs/mcp-servers.json - name: Cleanup Docker containers if: always() From 442f6756c3cf34fafb7eeef4cd48b7c0687f4fdf Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 7 Mar 2026 00:18:22 -0800 Subject: [PATCH 41/63] Update Claude workflow to use absolute path for mcp-servers configuration --- .github/workflows/claude.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index c8f48fbbf..f40d5474e 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -95,7 +95,7 @@ jobs: } claude_args: >- - --mcp-config .github/configs/mcp-servers.json + --mcp-config ${{ github.workspace }}/.github/configs/mcp-servers.json - name: Cleanup Docker containers if: always() From 02988fb986539d8b40351a7adffb8e1668b3c419 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 7 Mar 2026 01:12:10 -0800 Subject: [PATCH 42/63] Add environment specification for copilot-setup-steps job --- .github/workflows/copilot-setup-steps.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index afa699164..c93dbe46b 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -23,6 +23,7 @@ jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: runs-on: ubuntu-latest + environment: copilot # Set the permissions to the lowest permissions possible needed for your steps. # Copilot will be given its own token for its operations. From 239fa78bb1a3fba5428ffd499ff8072cc45f67e3 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 06:40:49 +0000 Subject: [PATCH 43/63] fix: replace DateTime stale-check with thread-safe Interlocked version counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stale-update guard in FetchMcpServerData and FetchAiAgentData used DateTime.UtcNow comparisons, which had three race conditions: 1. Same-tick blindness: DateTime.UtcNow has ~15ms resolution on Windows. Two calls within the same tick get identical timestamps, so the `_setTime > fetchTime` check evaluates to false for both — both concurrent fetches apply their results non-deterministically. 2. Missing memory barriers: _setMcpServerDataTime / _setAiAgentDataTime were plain DateTime fields written on the main thread and read on a thread-pool thread (ContinueWith) without volatile or Interlocked synchronization, giving no visibility guarantee. 3. TOCTOU window: the stale check runs on the thread pool but the UI update runs later via MainThread.Instance.Run. A newer update could arrive in that gap with no second check to catch it. Fix: - Replace DateTime fields with long version counters (_mcpServerDataVersion, _aiAgentDataVersion). - SetMcpServerData now calls Interlocked.Increment and returns the resulting version; FetchMcpServerData captures it as fetchVersion. - FetchAiAgentData calls Interlocked.Increment at the point where a valid async fetch is started, giving each call a unique version. - SetAiAgentStatus calls Interlocked.Increment so that OnClientsChanged events still supersede any in-flight FetchAiAgentData. - All ContinueWith checks now use Interlocked.Read(ref version) != fetchVersion. - A second version check is added inside MainThread.Instance.Run to close the TOCTOU window between the thread-pool check and callback execution. The warning log messages are preserved as-is (the warnings still fire when a newer update has genuinely superseded a stale fetch result). Co-authored-by: Ivan Murzak --- .../UI/Window/MainWindowEditor.CreateGUI.cs | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs index 1e2c34139..3b2f7087d 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using com.IvanMurzak.McpPlugin.Common.Model; using com.IvanMurzak.McpPlugin.Common.Utils; using com.IvanMurzak.McpPlugin.Skills; @@ -178,8 +179,8 @@ public partial class MainWindowEditor private VisualElement? _aiAgentLabelsContainer; private VisualElement? _aiAgentStatusCircle; - private DateTime _setMcpServerDataTime; - private DateTime _setAiAgentDataTime; + private long _mcpServerDataVersion; + private long _aiAgentDataVersion; protected override void OnGUICreated(VisualElement root) { @@ -231,7 +232,7 @@ private static void SetStatusIndicator(VisualElement element, string statusClass private void SetAiAgentStatus(bool isConnected, IEnumerable? labels = null) { - _setAiAgentDataTime = DateTime.UtcNow; + Interlocked.Increment(ref _aiAgentDataVersion); if (_aiAgentStatusCircle == null) { @@ -595,9 +596,9 @@ private static void HandleServerButton(Button btnStartStop, Label statusLabel) } } - private void SetMcpServerData(McpServerData? data, McpServerStatus status, Button btnStartStop, VisualElement statusCircle, Label statusLabel) + private long SetMcpServerData(McpServerData? data, McpServerStatus status, Button btnStartStop, VisualElement statusCircle, Label statusLabel) { - _setMcpServerDataTime = DateTime.UtcNow; + var version = Interlocked.Increment(ref _mcpServerDataVersion); if (Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Trace)) Logger.LogTrace("Setting MCP server data: {status}, Data: {data}", status, data?.ToPrettyJson() ?? "null"); @@ -608,12 +609,14 @@ private void SetMcpServerData(McpServerData? data, McpServerStatus status, Butto btnStartStop.SetEnabled(status == McpServerStatus.Running || status == McpServerStatus.Stopped); statusLabel.text = GetServerLabelText(status, data); SetStatusIndicator(statusCircle, GetServerStatusClass(status)); + return version; } private void FetchMcpServerData(McpServerStatus status, Button btnStartStop, VisualElement statusCircle, Label statusLabel) { - // Update UI immediately with current status - SetMcpServerData(null, status, btnStartStop, statusCircle, statusLabel); + // Update UI immediately with current status; capture the version atomically so that + // the async result can detect if a newer update has superseded it. + var fetchVersion = SetMcpServerData(null, status, btnStartStop, statusCircle, statusLabel); // Then try to fetch additional data asynchronously var mcpPluginInstance = UnityMcpPluginEditor.Instance.McpPluginInstance; @@ -630,7 +633,6 @@ private void FetchMcpServerData(McpServerStatus status, Button btnStartStop, Vis return; } - var fetchTime = DateTime.UtcNow; var task = mcpManagerHub.GetMcpServerData(); if (task == null) { @@ -640,14 +642,18 @@ private void FetchMcpServerData(McpServerStatus status, Button btnStartStop, Vis task.ContinueWith(t => { - if (_setMcpServerDataTime > fetchTime) + if (Interlocked.Read(ref _mcpServerDataVersion) != fetchVersion) { Logger.LogWarning("Skipping MCP server data update because a newer update was applied at {time}", - _setMcpServerDataTime); + DateTime.UtcNow); return; } MainThread.Instance.Run(() => { + // Second check: close the TOCTOU window between the thread-pool check above + // and the main-thread callback execution. + if (Interlocked.Read(ref _mcpServerDataVersion) != fetchVersion) + return; if (t.IsCompletedSuccessfully) { var data = t.Result; @@ -801,7 +807,6 @@ private void FetchAiAgentData(int retryCount = 3, int retryDelayMs = 3000) return; } - var fetchTime = DateTime.UtcNow; var task = mcpManagerHub.GetMcpClientData(); if (task == null) { @@ -809,16 +814,24 @@ private void FetchAiAgentData(int retryCount = 3, int retryDelayMs = 3000) return; } + // Claim a unique version for this fetch so the async result can detect if a newer + // update (e.g. from OnClientsChanged or another FetchAiAgentData call) has superseded it. + var fetchVersion = Interlocked.Increment(ref _aiAgentDataVersion); + task.ContinueWith(t => { - if (_setAiAgentDataTime > fetchTime) + if (Interlocked.Read(ref _aiAgentDataVersion) != fetchVersion) { Logger.LogWarning("Skipping AI agent data update because a newer update was applied at {time}", - _setAiAgentDataTime); + DateTime.UtcNow); return; } MainThread.Instance.Run(() => { + // Second check: close the TOCTOU window between the thread-pool check above + // and the main-thread callback execution. + if (Interlocked.Read(ref _aiAgentDataVersion) != fetchVersion) + return; if (t.IsCompletedSuccessfully) { var clients = t.Result; From 6fd0dd99aaf55ae9a5a78cd6451eabfee6586031 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Mon, 9 Mar 2026 02:11:36 -0700 Subject: [PATCH 44/63] refactor: change log level from Warning to Trace for MCP and AI agent data updates --- .../Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs index 3b2f7087d..456ae5812 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs @@ -644,7 +644,7 @@ private void FetchMcpServerData(McpServerStatus status, Button btnStartStop, Vis { if (Interlocked.Read(ref _mcpServerDataVersion) != fetchVersion) { - Logger.LogWarning("Skipping MCP server data update because a newer update was applied at {time}", + Logger.LogTrace("Skipping MCP server data update because a newer update was applied at {time}", DateTime.UtcNow); return; } @@ -822,7 +822,7 @@ private void FetchAiAgentData(int retryCount = 3, int retryDelayMs = 3000) { if (Interlocked.Read(ref _aiAgentDataVersion) != fetchVersion) { - Logger.LogWarning("Skipping AI agent data update because a newer update was applied at {time}", + Logger.LogTrace("Skipping AI agent data update because a newer update was applied at {time}", DateTime.UtcNow); return; } From 42395f00eccace56457b43e59d044159c7236e80 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Mon, 9 Mar 2026 02:26:46 -0700 Subject: [PATCH 45/63] feat: add settings.json files for Claude configuration in main and plugin directories --- .claude/settings.json | 31 ++++++++++++++++++++++++++ .gitignore | 2 +- Unity-MCP-Plugin/.claude/settings.json | 21 +++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .claude/settings.json create mode 100644 Unity-MCP-Plugin/.claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..0f824b7c7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,31 @@ +{ + "permissions": { + "allow": [ + "Bash(*)", + "WebBrowser", + "WebFetch(*)", + "WebSearch(*)", + "Read(*)", + "Write(*)", + "Edit(*)", + "Glob(*)", + "Grep(*)", + "Agent(*)", + "NotebookEdit(*)", + "TodoWrite", + "Skill(*)", + "TaskOutput(*)", + "TaskStop(*)", + "EnterPlanMode", + "ExitPlanMode", + "EnterWorktree", + "AskUserQuestion" + ], + "additionalDirectories": [ + "C:\\tmp" + ] + }, + "enabledPlugins": { + "csharp-lsp@claude-plugins-official": true + } +} diff --git a/.gitignore b/.gitignore index 9a10f7823..4dbfd385e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ .unity-license # AI configurations -.claude +**/.claude/settings.local.json .gemini .aider* .roo diff --git a/Unity-MCP-Plugin/.claude/settings.json b/Unity-MCP-Plugin/.claude/settings.json new file mode 100644 index 000000000..21dfd057e --- /dev/null +++ b/Unity-MCP-Plugin/.claude/settings.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "*" + ], + "deny": [], + "ask": [], + "additionalDirectories": [ + "../Installer", + "../Unity-MCP-Server", + "../Unity-MCP.wiki", + "../Unity-Tests", + "../commands", + "../docs" + ] + }, + "enableAllProjectMcpServers": true, + "enabledMcpjsonServers": [ + "ai-game-developer" + ] +} \ No newline at end of file From 26bc82afaf336037b930db0412e552cf084793ba Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Mon, 9 Mar 2026 02:30:01 -0700 Subject: [PATCH 46/63] refactor: simplify permissions in settings.json and add enabledPlugins section for plugin configuration --- .claude/settings.json | 30 ++++++-------------------- Unity-MCP-Plugin/.claude/settings.json | 5 ++++- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 0f824b7c7..fe3716485 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,31 +1,15 @@ { "permissions": { "allow": [ - "Bash(*)", - "WebBrowser", - "WebFetch(*)", - "WebSearch(*)", - "Read(*)", - "Write(*)", - "Edit(*)", - "Glob(*)", - "Grep(*)", - "Agent(*)", - "NotebookEdit(*)", - "TodoWrite", - "Skill(*)", - "TaskOutput(*)", - "TaskStop(*)", - "EnterPlanMode", - "ExitPlanMode", - "EnterWorktree", - "AskUserQuestion" + "*" ], - "additionalDirectories": [ - "C:\\tmp" - ] + "additionalDirectories": [] }, + "enableAllProjectMcpServers": true, + "enabledMcpjsonServers": [ + "ai-game-developer" + ], "enabledPlugins": { "csharp-lsp@claude-plugins-official": true } -} +} \ No newline at end of file diff --git a/Unity-MCP-Plugin/.claude/settings.json b/Unity-MCP-Plugin/.claude/settings.json index 21dfd057e..48ec1cdf8 100644 --- a/Unity-MCP-Plugin/.claude/settings.json +++ b/Unity-MCP-Plugin/.claude/settings.json @@ -17,5 +17,8 @@ "enableAllProjectMcpServers": true, "enabledMcpjsonServers": [ "ai-game-developer" - ] + ], + "enabledPlugins": { + "csharp-lsp@claude-plugins-official": true + } } \ No newline at end of file From ab27b1239b250a163c056c04c8a7a28cb5b3b9b5 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Mon, 9 Mar 2026 02:52:57 -0700 Subject: [PATCH 47/63] refactor: restrict permissions in settings.json to specific actions for improved security --- .claude/settings.json | 5 ++++- Unity-MCP-Plugin/.claude/settings.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index fe3716485..615a433f4 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,10 @@ { "permissions": { "allow": [ - "*" + "Bash(*)", + "Read(*)", + "WebFetch(*)", + "mcp__ai-game-developer__*" ], "additionalDirectories": [] }, diff --git a/Unity-MCP-Plugin/.claude/settings.json b/Unity-MCP-Plugin/.claude/settings.json index 48ec1cdf8..5df331995 100644 --- a/Unity-MCP-Plugin/.claude/settings.json +++ b/Unity-MCP-Plugin/.claude/settings.json @@ -1,7 +1,10 @@ { "permissions": { "allow": [ - "*" + "Bash(*)", + "Read(*)", + "WebFetch(*)", + "mcp__ai-game-developer__*" ], "deny": [], "ask": [], From 8672e1d84346697564362662ef3a6a30cb81a6c6 Mon Sep 17 00:00:00 2001 From: IvanMurzak Date: Mon, 9 Mar 2026 19:35:42 +0000 Subject: [PATCH 48/63] chore: bump version to 0.51.5 --- .../Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs | 2 +- README.md | 4 ++-- Unity-MCP-Plugin/Assets/root/README.md | 4 ++-- Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs | 2 +- Unity-MCP-Plugin/Assets/root/package.json | 2 +- Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj | 2 +- Unity-MCP-Server/server.json | 4 ++-- docs/README.es.md | 4 ++-- docs/README.ja.md | 4 ++-- docs/README.zh-CN.md | 4 ++-- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Installer/Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs b/Installer/Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs index 99d03bcbf..d45f6d0bb 100644 --- a/Installer/Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs +++ b/Installer/Assets/com.IvanMurzak/AI Game Dev Installer/Installer.cs @@ -16,7 +16,7 @@ namespace com.IvanMurzak.Unity.MCP.Installer public static partial class Installer { public const string PackageId = "com.ivanmurzak.unity.mcp"; - public const string Version = "0.51.4"; + public const string Version = "0.51.5"; static Installer() { diff --git a/README.md b/README.md index 1770bd351..0802b47da 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Unlike other tools, this plugin works **inside your compiled game**, allowing fo - ✔️ **Flexible deployment** - Works locally (stdio) and remotely (http) via configuration - ✔️ **Extensible** - Create [custom MCP Tools in your project code](#add-custom-mcp-tool) -[![DOWNLOAD INSTALLER](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) +[![DOWNLOAD INSTALLER](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage) ![AI Game Developer Windows](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -217,7 +217,7 @@ Install extensions when need more tools or [create your own](#add-custom-mcp-too ### Option 1 - Installer -- **[⬇️ Download Installer](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ Download Installer](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage)** - **📂 Import installer into Unity project** > - You can double-click on the file - Unity will open it automatically > - OR: Open Unity Editor first, then click on `Assets/Import Package/Custom Package`, and choose the file diff --git a/Unity-MCP-Plugin/Assets/root/README.md b/Unity-MCP-Plugin/Assets/root/README.md index 1770bd351..0802b47da 100644 --- a/Unity-MCP-Plugin/Assets/root/README.md +++ b/Unity-MCP-Plugin/Assets/root/README.md @@ -37,7 +37,7 @@ Unlike other tools, this plugin works **inside your compiled game**, allowing fo - ✔️ **Flexible deployment** - Works locally (stdio) and remotely (http) via configuration - ✔️ **Extensible** - Create [custom MCP Tools in your project code](#add-custom-mcp-tool) -[![DOWNLOAD INSTALLER](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) +[![DOWNLOAD INSTALLER](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage) ![AI Game Developer Windows](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -217,7 +217,7 @@ Install extensions when need more tools or [create your own](#add-custom-mcp-too ### Option 1 - Installer -- **[⬇️ Download Installer](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ Download Installer](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage)** - **📂 Import installer into Unity project** > - You can double-click on the file - Unity will open it automatically > - OR: Open Unity Editor first, then click on `Assets/Import Package/Custom Package`, and choose the file diff --git a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs index 2179729f7..dcff92638 100644 --- a/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs +++ b/Unity-MCP-Plugin/Assets/root/Runtime/UnityMcpPlugin.cs @@ -26,7 +26,7 @@ namespace com.IvanMurzak.Unity.MCP public partial class UnityMcpPlugin : IDisposable { - public const string Version = "0.51.4"; + public const string Version = "0.51.5"; private static int _singletonCount = 0; public static bool HasAnyInstance => _singletonCount > 0; diff --git a/Unity-MCP-Plugin/Assets/root/package.json b/Unity-MCP-Plugin/Assets/root/package.json index 0c702b732..ba7e20296 100644 --- a/Unity-MCP-Plugin/Assets/root/package.json +++ b/Unity-MCP-Plugin/Assets/root/package.json @@ -11,7 +11,7 @@ "MCP", "Unity MCP" ], - "version": "0.51.4", + "version": "0.51.5", "unity": "2022.3", "description": "AI-powered bridge connecting LLMs and advanced AI agents to the Unity Editor via the Model Context Protocol (MCP). Chat with AI to generate code, debug errors, and automate game development tasks directly within your project.", "dependencies": { diff --git a/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj b/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj index e54f7ec5f..fcf22b9e1 100644 --- a/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj +++ b/Unity-MCP-Server/com.IvanMurzak.Unity.MCP.Server.csproj @@ -13,7 +13,7 @@ true unity-mcp-server com.IvanMurzak.Unity.MCP.Server - 0.51.4 + 0.51.5 Ivan Murzak Ivan Murzak Copyright © 2025 Ivan Murzak diff --git a/Unity-MCP-Server/server.json b/Unity-MCP-Server/server.json index fb8208a54..8da4ee005 100644 --- a/Unity-MCP-Server/server.json +++ b/Unity-MCP-Server/server.json @@ -8,13 +8,13 @@ "source": "github", "subfolder": "Unity-MCP-Server" }, - "version": "0.51.4", + "version": "0.51.5", "packages": [ { "registry_type": "oci", "registry_base_url": "https://docker.io", "identifier": "ivanmurzakdev/unity-mcp-server", - "version": "0.51.4", + "version": "0.51.5", "transport": { "type": "stdio" }, diff --git a/docs/README.es.md b/docs/README.es.md index 4a5347ca4..21fa11a9e 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -37,7 +37,7 @@ A diferencia de otras herramientas, este plugin funciona **dentro de tu juego co - ✔️ **Despliegue flexible** - Funciona localmente (stdio) y remotamente (http) mediante configuración - ✔️ **Extensible** - Crea [Herramientas MCP personalizadas en el código de tu proyecto](#añadir-herramienta-mcp-personalizada) -[![DESCARGAR INSTALADOR](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) +[![DESCARGAR INSTALADOR](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage) ![Ventanas del Desarrollador de Juegos con IA](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -217,7 +217,7 @@ Instala extensiones cuando necesites más herramientas o [crea las tuyas propias ### Opción 1 - Instalador -- **[⬇️ Descargar Instalador](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ Descargar Instalador](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage)** - **📂 Importar el instalador en el proyecto de Unity** > - Puedes hacer doble clic en el archivo - Unity lo abrirá automáticamente > - O BIEN: Abre el Editor de Unity primero, luego haz clic en `Assets/Import Package/Custom Package` y elige el archivo diff --git a/docs/README.ja.md b/docs/README.ja.md index a932c34b5..9b9486815 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -37,7 +37,7 @@ - ✔️ **柔軟なデプロイ** - 設定によりローカル(stdio)およびリモート(http)で動作 - ✔️ **拡張可能** - [プロジェクトコードにカスタム MCP ツールを作成](#カスタム-mcp-ツールの追加)可能 -[![インストーラーをダウンロード](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) +[![インストーラーをダウンロード](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage) ![AI Game Developer Windows](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -217,7 +217,7 @@ ### オプション 1 - インストーラー -- **[⬇️ インストーラーをダウンロード](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ インストーラーをダウンロード](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage)** - **📂 Unity プロジェクトにインストーラーをインポート** > - ファイルをダブルクリックすると Unity が自動的に開きます > - または: Unity Editor を先に開き、`Assets/Import Package/Custom Package` をクリックしてファイルを選択 diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 1324d25c4..0cdc8f5c1 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -37,7 +37,7 @@ - ✔️ **灵活部署** — 支持本地(stdio)和远程(http)两种配置方式 - ✔️ **可扩展** — 在项目代码中[创建自定义 MCP 工具](#添加自定义-mcp-tool) -[![下载安装器](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage) +[![下载安装器](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/button/button_download.svg?raw=true)](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage) ![AI 游戏开发者 Windows](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/editor/ai-game-developer-windows.png?raw=true) @@ -217,7 +217,7 @@ ### 选项 1 — 安装器 -- **[⬇️ 下载安装器](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.4/AI-Game-Dev-Installer.unitypackage)** +- **[⬇️ 下载安装器](https://github.com/IvanMurzak/Unity-MCP/releases/download/0.51.5/AI-Game-Dev-Installer.unitypackage)** - **📂 将安装器导入 Unity 项目** > - 双击文件 — Unity 将自动打开它 > - 或者:先打开 Unity 编辑器,然后点击 `Assets/Import Package/Custom Package`,选择文件 From c9efe97813546c1d34a750ff70c19732f1ac9699 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Mon, 9 Mar 2026 12:44:14 -0700 Subject: [PATCH 49/63] chore: update package version to 0.51.4 and bump dependencies to latest versions --- CLAUDE.md | 9 +++++++-- Unity-MCP-Plugin/CLAUDE.md | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a3eb4be9a..8017b40d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,14 +54,14 @@ dotnet run --project com.IvanMurzak.Unity.MCP.Server.csproj -- --client-transpor ### MCP Inspector (debugging) ```bash -Commands/start_mcp_inspector.bat # requires Node.js +Unity-MCP-Plugin/Commands/start_mcp_inspector.bat # requires Node.js ``` ## Release & Versioning Version is sourced from [Unity-MCP-Plugin/Assets/root/package.json](Unity-MCP-Plugin/Assets/root/package.json). -- **Bump version**: `.\bump-version.ps1 ` — updates all version references across the repo +- **Bump version**: `.\commands\bump-version.ps1 ` — updates all version references across the repo - **Release**: Push to `main` triggers `.github/workflows/release.yml`, which runs the 18-combination Unity test matrix (3 Unity versions × 3 test modes × 2 OS), publishes executables to GitHub Releases, Docker Hub, and NuGet ## CI/CD @@ -70,8 +70,13 @@ Version is sourced from [Unity-MCP-Plugin/Assets/root/package.json](Unity-MCP-Pl |---|---|---| | `release.yml` | Push to `main` | Full release pipeline | | `test_pull_request.yml` | PR to `main`/`dev` | Validates all 18 test matrix combinations | +| `test_pull_request_manual.yml` | Manual | Manual trigger for PR test matrix | +| `test_unity_plugin.yml` | Reusable | Unity plugin test runner (called by other workflows) | | `deploy.yml` | Release published / manual | NuGet + Docker Hub deploy | | `deploy_server_executables.yml` | Release published | Cross-platform binary upload | +| `bump_version.yml` | Manual | Automated version bumping | +| `claude.yml` | Issue/PR comments | Claude Code AI assistant | +| `copilot-setup-steps.yml` | Reusable | GitHub Copilot setup steps | PRs from untrusted contributors require a `ci-ok` label from a maintainer before CI runs. diff --git a/Unity-MCP-Plugin/CLAUDE.md b/Unity-MCP-Plugin/CLAUDE.md index 67de9d919..008ba2dcf 100644 --- a/Unity-MCP-Plugin/CLAUDE.md +++ b/Unity-MCP-Plugin/CLAUDE.md @@ -13,7 +13,7 @@ Unity-MCP is a bridge between Large Language Models (LLMs) and Unity Editor that - **Reflection-based Tools**: Dynamic access to Unity API using ReflectorNet - **AI Agent Configurators**: Auto-configuration system for 10 AI clients (Claude Desktop, Cursor, VS Code Copilot, Gemini, etc.) -**Package**: `com.ivanmurzak.unity.mcp` (current version: `0.45.0`) +**Package**: `com.ivanmurzak.unity.mcp` (current version: `0.51.4`) ## Development Commands @@ -235,9 +235,9 @@ Key packages (via OpenUPM `org.nuget.*` scope): - **McpPlugin** (bundled DLL): MCP protocol implementation and plugin framework - **ReflectorNet** (bundled DLL): Advanced reflection system for Unity objects -- **SignalR Client** `10.0.1`: Real-time communication (abstracted via `IMcpPlugin`) +- **SignalR Client** `10.0.3`: Real-time communication (abstracted via `IMcpPlugin`) - **Roslyn** `4.14.0`: C# code compilation and execution - **R3** `1.3.0`: Reactive programming (`ReactiveProperty`, `Subject`, `Observable`) -- **System.Text.Json** `10.0.1`: JSON serialization -- **Microsoft.Extensions.Hosting** `10.0.1`: Server hosting infrastructure -- **Microsoft.Extensions.Logging** `10.0.1`: Logging abstractions +- **System.Text.Json** `10.0.3`: JSON serialization +- **Microsoft.Extensions.Hosting** `10.0.3`: Server hosting infrastructure +- **Microsoft.Extensions.Logging** `10.0.3`: Logging abstractions From fb3088ff38de27c491b6c07d2de5c7a8fd3d349b Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Tue, 10 Mar 2026 17:12:42 -0700 Subject: [PATCH 50/63] fix: handle exceptions when creating metadata references for assemblies --- .../Editor/Scripts/API/Tool/Script.Execute.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs index 5dd77104f..d7bd7d938 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs @@ -131,7 +131,23 @@ static bool ExecuteCSharpCode( references: AssemblyUtils.AllAssemblies .Where(a => !a.IsDynamic) // Exclude dynamic assemblies .Where(a => !string.IsNullOrEmpty(a.Location)) - .Select(a => MetadataReference.CreateFromFile(a.Location)) + .Select(a => + { + try { return MetadataReference.CreateFromFile(a.Location); } + catch (DirectoryNotFoundException ex) + { + logger?.LogWarning("Directory not found for assembly '{AssemblyName}' at '{Location}': {Error}", + a.GetName().Name, a.Location, ex.Message); + return null; + } + catch (Exception ex) + { + logger?.LogWarning("Failed to load metadata reference for assembly '{AssemblyName}' at '{Location}': {Error}", + a.GetName().Name, a.Location, ex.Message); + return null; + } + }) + .Where(r => r != null) .ToArray(), options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) ); From 2b9488573790476afba2306855468fa44b416fc3 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Tue, 10 Mar 2026 17:23:10 -0700 Subject: [PATCH 51/63] fix: improve logging for assembly metadata reference loading errors --- .../root/Editor/Scripts/API/Tool/Script.Execute.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs index d7bd7d938..3a3100092 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs @@ -136,18 +136,25 @@ static bool ExecuteCSharpCode( try { return MetadataReference.CreateFromFile(a.Location); } catch (DirectoryNotFoundException ex) { - logger?.LogWarning("Directory not found for assembly '{AssemblyName}' at '{Location}': {Error}", + logger?.LogWarning(ex, "Directory not found for assembly '{AssemblyName}' at '{Location}': {Error}", + a.GetName().Name, a.Location, ex.Message); + return null; + } + catch (FileNotFoundException ex) + { + logger?.LogWarning(ex, "File not found for assembly '{AssemblyName}' at '{Location}': {Error}", a.GetName().Name, a.Location, ex.Message); return null; } catch (Exception ex) { - logger?.LogWarning("Failed to load metadata reference for assembly '{AssemblyName}' at '{Location}': {Error}", + logger?.LogWarning(ex, "Failed to load metadata reference for assembly '{AssemblyName}' at '{Location}': {Error}", a.GetName().Name, a.Location, ex.Message); return null; } }) .Where(r => r != null) + .OfType() .ToArray(), options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) ); From 3b133fb7cca28eae5b67e5001a29fc620c57cdc6 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Tue, 10 Mar 2026 17:23:58 -0700 Subject: [PATCH 52/63] fix: enhance error handling for assembly metadata reference creation --- .../Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs index 3a3100092..23fbad745 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs @@ -133,7 +133,10 @@ static bool ExecuteCSharpCode( .Where(a => !string.IsNullOrEmpty(a.Location)) .Select(a => { - try { return MetadataReference.CreateFromFile(a.Location); } + try + { + return MetadataReference.CreateFromFile(a.Location); + } catch (DirectoryNotFoundException ex) { logger?.LogWarning(ex, "Directory not found for assembly '{AssemblyName}' at '{Location}': {Error}", From ce80626ec56ccd7fd79dc49f6bd8af19713de4bf Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Tue, 10 Mar 2026 22:45:19 -0700 Subject: [PATCH 53/63] fix: remove null filtering from metadata references collection --- .../Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs index 23fbad745..2d7c40c05 100644 --- a/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs +++ b/Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Script.Execute.cs @@ -156,7 +156,6 @@ static bool ExecuteCSharpCode( return null; } }) - .Where(r => r != null) .OfType() .ToArray(), options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) From 408691ffcb80d22b268fd9f94fd822914894e9ac Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Tue, 10 Mar 2026 23:25:14 -0700 Subject: [PATCH 54/63] fix: update Unity workflow configurations for improved security and functionality --- .github/workflows/release.yml | 2 -- .github/workflows/test_pull_request.yml | 4 ++- .github/workflows/test_unity_plugin.yml | 45 +++++++++++++++---------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c2d3595a..1b1b191cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,6 @@ jobs: - name: Test Unity Installer (EditMode) uses: game-ci/unity-test-runner@v4 env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: @@ -87,7 +86,6 @@ jobs: - name: Export Unity Package uses: game-ci/unity-builder@v4 env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: diff --git a/.github/workflows/test_pull_request.yml b/.github/workflows/test_pull_request.yml index a59f72894..2b43fd343 100644 --- a/.github/workflows/test_pull_request.yml +++ b/.github/workflows/test_pull_request.yml @@ -10,7 +10,7 @@ name: test-pull-request on: workflow_dispatch: - pull_request: + pull_request_target: branches: [main, dev] types: [opened, synchronize, reopened] @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Setup .NET uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index 54f153ba7..d3255840c 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -18,18 +18,14 @@ on: unityVersion: { required: true, type: string } testMode: { required: true, type: string } secrets: - UNITY_LICENSE: { required: true } - UNITY_EMAIL: { required: true } - UNITY_PASSWORD: { required: true } + UNITY_EMAIL: { required: false } + UNITY_PASSWORD: { required: false } ############################################################################## -# 2. Job – runs only after a maintainer applies the `ci-ok` label +# 2. Job ############################################################################## jobs: test: - if: | - github.event_name != 'pull_request_target' || - contains(github.event.pull_request.labels.*.name,'ci-ok') strategy: fail-fast: false matrix: @@ -45,25 +41,41 @@ jobs: steps: # --------------------------------------------------------------------- # - # 2-a. (PR only) abort if the contributor also changed workflow files + # 2-a. Checkout base branch (always safe) + # --------------------------------------------------------------------- # + - uses: actions/checkout@v5 + with: + lfs: false + + # --------------------------------------------------------------------- # + # 2-b. (Fork PRs only) abort if the contributor changed workflow files # --------------------------------------------------------------------- # - name: Abort if workflow files modified - if: ${{ github.event_name == 'pull_request_target' }} + if: | + github.event_name == ‘pull_request_target’ && + github.event.pull_request.head.repo.full_name != github.repository + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - git fetch --depth=1 origin "${{ github.base_ref }}" - if git diff --name-only HEAD origin/${{ github.base_ref }} | grep -q '^\.github/workflows/'; then - echo "::error::This PR edits workflow files – refusing to run with secrets"; exit 1; + git fetch --depth=1 origin "$PR_HEAD_SHA" + if git diff --name-only HEAD FETCH_HEAD | grep -q ‘^\.github/’; then + echo "::error::This PR edits workflow/action files – refusing to run" + exit 1 fi # --------------------------------------------------------------------- # - # 2-b. Checkout the contributor’s commit safely + # 2-c. (Fork PRs only) checkout PR head after safety check # --------------------------------------------------------------------- # - uses: actions/checkout@v5 + if: | + github.event_name == ‘pull_request_target’ && + github.event.pull_request.head.repo.full_name != github.repository with: + ref: ${{ github.event.pull_request.head.sha }} lfs: false # --------------------------------------------------------------------- # - # 2-c. Cache & run the Unity test-runner + # 2-d. Cache & run the Unity test-runner # --------------------------------------------------------------------- # - name: Generate cache key id: cache_key @@ -89,9 +101,8 @@ jobs: - uses: game-ci/unity-test-runner@v4 id: tests env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL || 'UnityEngineTester@gmail.com' }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD || 'ZUq3YR6qM1' }} with: projectPath: ${{ inputs.projectPath }} unityVersion: ${{ inputs.unityVersion }} From a29b9f3c9256995255e6297800fa2c8596fced75 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Tue, 10 Mar 2026 23:27:33 -0700 Subject: [PATCH 55/63] fix: update pull request trigger settings in workflow configuration --- .github/workflows/test_pull_request.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test_pull_request.yml b/.github/workflows/test_pull_request.yml index 2b43fd343..32dab9bd4 100644 --- a/.github/workflows/test_pull_request.yml +++ b/.github/workflows/test_pull_request.yml @@ -10,6 +10,9 @@ name: test-pull-request on: workflow_dispatch: + pull_request: + branches: [main, dev] + types: [opened, synchronize, reopened] pull_request_target: branches: [main, dev] types: [opened, synchronize, reopened] From dec4043ba1506fe0ddd647b6e58f79801be1c1c6 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Tue, 10 Mar 2026 23:38:00 -0700 Subject: [PATCH 56/63] fix: correct syntax for pull request workflow file checks --- .github/workflows/test_unity_plugin.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index d3255840c..9e949db3a 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -51,15 +51,13 @@ jobs: # 2-b. (Fork PRs only) abort if the contributor changed workflow files # --------------------------------------------------------------------- # - name: Abort if workflow files modified - if: | - github.event_name == ‘pull_request_target’ && - github.event.pull_request.head.repo.full_name != github.repository + if: "github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository" env: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | git fetch --depth=1 origin "$PR_HEAD_SHA" - if git diff --name-only HEAD FETCH_HEAD | grep -q ‘^\.github/’; then - echo "::error::This PR edits workflow/action files – refusing to run" + if git diff --name-only HEAD FETCH_HEAD | grep -q '^\.github/'; then + echo "::error::This PR edits workflow/action files - refusing to run" exit 1 fi @@ -67,9 +65,7 @@ jobs: # 2-c. (Fork PRs only) checkout PR head after safety check # --------------------------------------------------------------------- # - uses: actions/checkout@v5 - if: | - github.event_name == ‘pull_request_target’ && - github.event.pull_request.head.repo.full_name != github.repository + if: "github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository" with: ref: ${{ github.event.pull_request.head.sha }} lfs: false From 7fb543a714aa1552f3602bfbbc8c09456290cc2c Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 11 Mar 2026 00:23:58 -0700 Subject: [PATCH 57/63] feat: add Unity license activation action and integrate into workflows --- .github/actions/setup-unity-mcp/action.yml | 98 +++++----------- .../actions/unity/activate-license/action.yml | 105 ++++++++++++++++++ .github/workflows/release.yml | 10 ++ .github/workflows/test_unity_plugin.yml | 18 ++- 4 files changed, 156 insertions(+), 75 deletions(-) create mode 100644 .github/actions/unity/activate-license/action.yml diff --git a/.github/actions/setup-unity-mcp/action.yml b/.github/actions/setup-unity-mcp/action.yml index d0d48571a..87a89802f 100644 --- a/.github/actions/setup-unity-mcp/action.yml +++ b/.github/actions/setup-unity-mcp/action.yml @@ -12,10 +12,16 @@ description: "Start MCP Server and Unity Editor in Docker containers with licens inputs: unity-email: description: "Unity account email for license activation" - required: true + required: false + default: "" unity-password: description: "Unity account password for license activation" - required: true + required: false + default: "" + unity-license: + description: "Unity license file content (ULF XML). If provided, skips activation." + required: false + default: "" unity-project-path: description: "Relative path to the Unity project folder" required: false @@ -73,81 +79,33 @@ runs: done shell: bash - - name: Setup Node.js - uses: actions/setup-node@v4 + # --------------------------------------------------------------------- # + # License activation: use provided license or activate from credentials + # --------------------------------------------------------------------- # + - name: Activate Unity license + if: inputs.unity-license == '' + id: activate + uses: ./.github/actions/unity/activate-license with: - node-version: "20" + unityVersion: ${{ steps.unity-version.outputs.unity_version }} + unity-email: ${{ inputs.unity-email }} + unity-password: ${{ inputs.unity-password }} - - name: Install unity-license-activate - run: npm install --global unity-license-activate - shell: bash - - - name: Generate Unity activation file (.alf) - id: alf - env: - UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} + - name: Write Unity license file run: | mkdir -p "${GITHUB_WORKSPACE}/.unity-license" - - docker run --rm \ - -v "${GITHUB_WORKSPACE}/.unity-license:/output" \ - -v "${GITHUB_WORKSPACE}/.unity-license:/root/.local/share/unity3d/Unity/" \ - -w /output \ - unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ - unity-editor -batchmode -createManualActivationFile -logFile /dev/stdout || true - - ALF_FILE=$(find "${GITHUB_WORKSPACE}/.unity-license" -name "*.alf" | head -1) - if [ -z "$ALF_FILE" ]; then - echo "Failed to generate .alf file" - exit 1 + if [ -n "$LICENSE_FROM_INPUT" ]; then + echo "$LICENSE_FROM_INPUT" > "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" + echo "Unity license written from input" + else + echo "$LICENSE_FROM_ACTIVATION" > "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" + echo "Unity license written from activation" fi - echo "Generated ALF file: $ALF_FILE" - echo "alf_path=${ALF_FILE}" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Activate Unity license - run: | - MAX_RETRIES=5 - for attempt in $(seq 1 $MAX_RETRIES); do - echo "=== License activation attempt $attempt of $MAX_RETRIES ===" - if unity-license-activate \ - "${{ inputs.unity-email }}" \ - "${{ inputs.unity-password }}" \ - "${{ steps.alf.outputs.alf_path }}"; then - echo "unity-license-activate succeeded on attempt $attempt" - break - fi - if [ "$attempt" -eq "$MAX_RETRIES" ]; then - echo "All $MAX_RETRIES license activation attempts failed" - exit 1 - fi - echo "Attempt $attempt failed, retrying in 10s..." - sleep 10 - done - - echo "--- Searching for .ulf file in workspace root ---" - find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" -ls 2>/dev/null - - ULF_FILE=$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -name "*.ulf" | head -1) - if [ -z "$ULF_FILE" ]; then - echo "Failed to obtain .ulf license file" - exit 1 - fi - echo "Found ULF file: $ULF_FILE" - cp "$ULF_FILE" "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" - echo "--- License directory contents ---" - ls -la "${GITHUB_WORKSPACE}/.unity-license/" - echo "Unity license activated successfully" + env: + LICENSE_FROM_INPUT: ${{ inputs.unity-license }} + LICENSE_FROM_ACTIVATION: ${{ steps.activate.outputs.license }} shell: bash - - name: Upload error screenshot - if: failure() - uses: actions/upload-artifact@v4 - with: - name: unity-license-error - path: error.png - if-no-files-found: ignore - - name: Start Unity Editor in background env: UNITY_VERSION: ${{ steps.unity-version.outputs.unity_version }} diff --git a/.github/actions/unity/activate-license/action.yml b/.github/actions/unity/activate-license/action.yml new file mode 100644 index 000000000..e58775531 --- /dev/null +++ b/.github/actions/unity/activate-license/action.yml @@ -0,0 +1,105 @@ +# ┌──────────────────────────────────────────────────────────────────┐ +# │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +# │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ +# │ Copyright (c) 2025 Ivan Murzak │ +# │ Licensed under the Apache License, Version 2.0. │ +# │ See the LICENSE file in the project root for more information. │ +# └──────────────────────────────────────────────────────────────────┘ + +name: "Unity Activate License" +description: "Activate a Unity Personal license via unity-license-activate and output the ULF content" + +inputs: + unityVersion: + description: "Unity version to activate (e.g. 2022.3.62f3)" + required: true + unity-email: + description: "Unity account email" + required: false + default: "UnityEngineTester@gmail.com" + unity-password: + description: "Unity account password" + required: false + default: "ZUq3YR6qM1" + +outputs: + license: + description: "Unity license file content (ULF XML)" + value: ${{ steps.ulf.outputs.content }} + +runs: + using: "composite" + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install unity-license-activate + run: npm install --global unity-license-activate + shell: bash + + - name: Generate Unity activation file (.alf) + id: alf + env: + UNITY_VERSION: ${{ inputs.unityVersion }} + run: | + mkdir -p unity-license + + docker run --rm \ + -v "$(pwd)/unity-license:/output" \ + -w /output \ + unityci/editor:ubuntu-${UNITY_VERSION}-base-3 \ + unity-editor -batchmode -createManualActivationFile -logFile /dev/stdout || true + + ALF_FILE=$(find unity-license -name "*.alf" | head -1) + if [ -z "$ALF_FILE" ]; then + echo "Failed to generate .alf file" + exit 1 + fi + echo "Generated ALF file: $ALF_FILE" + echo "alf_path=${ALF_FILE}" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Activate Unity license + id: ulf + env: + UNITY_EMAIL: ${{ inputs.unity-email }} + UNITY_PASSWORD: ${{ inputs.unity-password }} + run: | + MAX_RETRIES=5 + for attempt in $(seq 1 $MAX_RETRIES); do + echo "=== License activation attempt $attempt of $MAX_RETRIES ===" + if unity-license-activate "$UNITY_EMAIL" "$UNITY_PASSWORD" "${{ steps.alf.outputs.alf_path }}"; then + echo "unity-license-activate succeeded on attempt $attempt" + break + fi + if [ "$attempt" -eq "$MAX_RETRIES" ]; then + echo "All $MAX_RETRIES license activation attempts failed" + exit 1 + fi + echo "Attempt $attempt failed, retrying in 10s..." + sleep 10 + done + + ULF_FILE=$(find . -maxdepth 2 -name "*.ulf" | head -1) + if [ -z "$ULF_FILE" ]; then + echo "Failed to obtain .ulf license file" + exit 1 + fi + echo "Found ULF file: $ULF_FILE" + + { + echo "content<> "$GITHUB_OUTPUT" + shell: bash + + - name: Upload error screenshot + if: failure() + uses: actions/upload-artifact@v4 + with: + name: unity-license-error-${{ inputs.unityVersion }} + path: error.png + if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b1b191cc..1df2692dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 + - name: Activate Unity license + id: activate + uses: ./.github/actions/unity/activate-license + with: + unityVersion: "2022.3.62f3" + unity-email: ${{ secrets.UNITY_EMAIL }} + unity-password: ${{ secrets.UNITY_PASSWORD }} + - name: Cache Unity Library uses: actions/cache@v4 with: @@ -60,6 +68,7 @@ jobs: - name: Test Unity Installer (EditMode) uses: game-ci/unity-test-runner@v4 env: + UNITY_LICENSE: ${{ steps.activate.outputs.license }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: @@ -86,6 +95,7 @@ jobs: - name: Export Unity Package uses: game-ci/unity-builder@v4 env: + UNITY_LICENSE: ${{ steps.activate.outputs.license }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index 9e949db3a..d2b7056c7 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -35,10 +35,6 @@ jobs: name: ${{ inputs.unityVersion }} ${{ inputs.testMode }} on ${{ matrix.platform }} runs-on: ${{ matrix.os }} - # permissions: # minimize the default token - # contents: write - # pull-requests: write - steps: # --------------------------------------------------------------------- # # 2-a. Checkout base branch (always safe) @@ -71,7 +67,18 @@ jobs: lfs: false # --------------------------------------------------------------------- # - # 2-d. Cache & run the Unity test-runner + # 2-d. Activate Unity license + # --------------------------------------------------------------------- # + - name: Activate Unity license + id: activate + uses: ./.github/actions/unity/activate-license + with: + unityVersion: ${{ inputs.unityVersion }} + unity-email: ${{ secrets.UNITY_EMAIL || 'UnityEngineTester@gmail.com' }} + unity-password: ${{ secrets.UNITY_PASSWORD || 'ZUq3YR6qM1' }} + + # --------------------------------------------------------------------- # + # 2-e. Cache & run the Unity test-runner # --------------------------------------------------------------------- # - name: Generate cache key id: cache_key @@ -97,6 +104,7 @@ jobs: - uses: game-ci/unity-test-runner@v4 id: tests env: + UNITY_LICENSE: ${{ steps.activate.outputs.license }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL || 'UnityEngineTester@gmail.com' }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD || 'ZUq3YR6qM1' }} with: From 1e373e9fe2b52d2d51798300e08d1495e505d65c Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 11 Mar 2026 00:35:02 -0700 Subject: [PATCH 58/63] fix: update Unity license handling to use base64 encoding and decoding --- .github/actions/setup-unity-mcp/action.yml | 4 ++-- .github/actions/unity/activate-license/action.yml | 9 +++------ .github/workflows/release.yml | 14 ++++++++++++-- .github/workflows/test_unity_plugin.yml | 14 +++++++++++++- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/actions/setup-unity-mcp/action.yml b/.github/actions/setup-unity-mcp/action.yml index 87a89802f..b825b8220 100644 --- a/.github/actions/setup-unity-mcp/action.yml +++ b/.github/actions/setup-unity-mcp/action.yml @@ -98,12 +98,12 @@ runs: echo "$LICENSE_FROM_INPUT" > "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" echo "Unity license written from input" else - echo "$LICENSE_FROM_ACTIVATION" > "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" + echo "$LICENSE_B64" | base64 -d > "${GITHUB_WORKSPACE}/.unity-license/Unity_lic.ulf" echo "Unity license written from activation" fi env: LICENSE_FROM_INPUT: ${{ inputs.unity-license }} - LICENSE_FROM_ACTIVATION: ${{ steps.activate.outputs.license }} + LICENSE_B64: ${{ steps.activate.outputs.license }} shell: bash - name: Start Unity Editor in background diff --git a/.github/actions/unity/activate-license/action.yml b/.github/actions/unity/activate-license/action.yml index e58775531..d05559687 100644 --- a/.github/actions/unity/activate-license/action.yml +++ b/.github/actions/unity/activate-license/action.yml @@ -24,7 +24,7 @@ inputs: outputs: license: - description: "Unity license file content (ULF XML)" + description: "Unity license file content (ULF XML, base64-encoded)" value: ${{ steps.ulf.outputs.content }} runs: @@ -89,11 +89,8 @@ runs: fi echo "Found ULF file: $ULF_FILE" - { - echo "content<> "$GITHUB_OUTPUT" + encoded=$(base64 -w 0 "$ULF_FILE") + echo "content=$encoded" >> "$GITHUB_OUTPUT" shell: bash - name: Upload error screenshot diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1df2692dd..24d3d40eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,18 @@ jobs: unity-email: ${{ secrets.UNITY_EMAIL }} unity-password: ${{ secrets.UNITY_PASSWORD }} + - name: Decode Unity license + run: | + DELIM="UNITY_LIC_$(openssl rand -hex 8)" + echo "$LICENSE_B64" | base64 -d > /tmp/unity_license.ulf + { + echo "UNITY_LICENSE<<$DELIM" + cat /tmp/unity_license.ulf + echo "$DELIM" + } >> "$GITHUB_ENV" + env: + LICENSE_B64: ${{ steps.activate.outputs.license }} + - name: Cache Unity Library uses: actions/cache@v4 with: @@ -68,7 +80,6 @@ jobs: - name: Test Unity Installer (EditMode) uses: game-ci/unity-test-runner@v4 env: - UNITY_LICENSE: ${{ steps.activate.outputs.license }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: @@ -95,7 +106,6 @@ jobs: - name: Export Unity Package uses: game-ci/unity-builder@v4 env: - UNITY_LICENSE: ${{ steps.activate.outputs.license }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index d2b7056c7..777bf0c2f 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -101,10 +101,22 @@ jobs: run: echo "image=unityci/editor:ubuntu-${{ inputs.unityVersion }}-${{ matrix.platform }}-3" >> $GITHUB_OUTPUT shell: bash + - name: Decode Unity license + run: | + DELIM="UNITY_LIC_$(openssl rand -hex 8)" + echo "$LICENSE_B64" | base64 -d > /tmp/unity_license.ulf + { + echo "UNITY_LICENSE<<$DELIM" + cat /tmp/unity_license.ulf + echo "$DELIM" + } >> "$GITHUB_ENV" + env: + LICENSE_B64: ${{ steps.activate.outputs.license }} + shell: bash + - uses: game-ci/unity-test-runner@v4 id: tests env: - UNITY_LICENSE: ${{ steps.activate.outputs.license }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL || 'UnityEngineTester@gmail.com' }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD || 'ZUq3YR6qM1' }} with: From 8288714a67d8f120c484a5b17295e383c819d88f Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 11 Mar 2026 00:40:31 -0700 Subject: [PATCH 59/63] fix: simplify Unity license decoding and environment variable setup --- .github/workflows/test_unity_plugin.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index 777bf0c2f..92ebb12f5 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -103,13 +103,8 @@ jobs: - name: Decode Unity license run: | - DELIM="UNITY_LIC_$(openssl rand -hex 8)" echo "$LICENSE_B64" | base64 -d > /tmp/unity_license.ulf - { - echo "UNITY_LICENSE<<$DELIM" - cat /tmp/unity_license.ulf - echo "$DELIM" - } >> "$GITHUB_ENV" + echo "UNITY_LICENSE=$(cat /tmp/unity_license.ulf | base64 -w 0)" >> "$GITHUB_ENV" env: LICENSE_B64: ${{ steps.activate.outputs.license }} shell: bash From 2b59de5d1b1d7ad777a0d156d0ae1cf0886447e0 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 11 Mar 2026 00:41:05 -0700 Subject: [PATCH 60/63] fix: update Unity license decoding to use delimiter for environment variable --- .github/workflows/test_unity_plugin.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index 92ebb12f5..df252978e 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -104,7 +104,10 @@ jobs: - name: Decode Unity license run: | echo "$LICENSE_B64" | base64 -d > /tmp/unity_license.ulf - echo "UNITY_LICENSE=$(cat /tmp/unity_license.ulf | base64 -w 0)" >> "$GITHUB_ENV" + DELIM="UNITY_LIC_EOF_$(openssl rand -hex 16)" + echo "UNITY_LICENSE<<$DELIM" >> "$GITHUB_ENV" + cat /tmp/unity_license.ulf >> "$GITHUB_ENV" + printf '\n%s\n' "$DELIM" >> "$GITHUB_ENV" env: LICENSE_B64: ${{ steps.activate.outputs.license }} shell: bash From adcb3f598e9b223ef062f9c6975ce793178eb237 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 11 Mar 2026 00:43:31 -0700 Subject: [PATCH 61/63] fix: improve Unity license decoding and environment variable setup --- .github/workflows/release.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24d3d40eb..b815607bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,13 +61,11 @@ jobs: - name: Decode Unity license run: | - DELIM="UNITY_LIC_$(openssl rand -hex 8)" echo "$LICENSE_B64" | base64 -d > /tmp/unity_license.ulf - { - echo "UNITY_LICENSE<<$DELIM" - cat /tmp/unity_license.ulf - echo "$DELIM" - } >> "$GITHUB_ENV" + DELIM="UNITY_LIC_EOF_$(openssl rand -hex 16)" + echo "UNITY_LICENSE<<$DELIM" >> "$GITHUB_ENV" + cat /tmp/unity_license.ulf >> "$GITHUB_ENV" + printf '\n%s\n' "$DELIM" >> "$GITHUB_ENV" env: LICENSE_B64: ${{ steps.activate.outputs.license }} From dfa1699b030af139a62229412adfd7cb138d4416 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 11 Mar 2026 01:15:58 -0700 Subject: [PATCH 62/63] fix: update workflow files to streamline pull request handling and unify license activation --- .github/workflows/release.yml | 1 + .github/workflows/test_pull_request.yml | 6 +---- .github/workflows/test_unity_plugin.yml | 29 +++---------------------- 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b815607bb..44f4a216b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,6 +68,7 @@ jobs: printf '\n%s\n' "$DELIM" >> "$GITHUB_ENV" env: LICENSE_B64: ${{ steps.activate.outputs.license }} + shell: bash - name: Cache Unity Library uses: actions/cache@v4 diff --git a/.github/workflows/test_pull_request.yml b/.github/workflows/test_pull_request.yml index 32dab9bd4..272e192cc 100644 --- a/.github/workflows/test_pull_request.yml +++ b/.github/workflows/test_pull_request.yml @@ -10,12 +10,10 @@ name: test-pull-request on: workflow_dispatch: + pull_request: branches: [main, dev] types: [opened, synchronize, reopened] - pull_request_target: - branches: [main, dev] - types: [opened, synchronize, reopened] jobs: build-and-zip-mcp-server: @@ -23,8 +21,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Setup .NET uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml index df252978e..c58d10e9b 100644 --- a/.github/workflows/test_unity_plugin.yml +++ b/.github/workflows/test_unity_plugin.yml @@ -37,37 +37,14 @@ jobs: steps: # --------------------------------------------------------------------- # - # 2-a. Checkout base branch (always safe) + # 2-a. Checkout repository # --------------------------------------------------------------------- # - uses: actions/checkout@v5 with: lfs: false # --------------------------------------------------------------------- # - # 2-b. (Fork PRs only) abort if the contributor changed workflow files - # --------------------------------------------------------------------- # - - name: Abort if workflow files modified - if: "github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository" - env: - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - git fetch --depth=1 origin "$PR_HEAD_SHA" - if git diff --name-only HEAD FETCH_HEAD | grep -q '^\.github/'; then - echo "::error::This PR edits workflow/action files - refusing to run" - exit 1 - fi - - # --------------------------------------------------------------------- # - # 2-c. (Fork PRs only) checkout PR head after safety check - # --------------------------------------------------------------------- # - - uses: actions/checkout@v5 - if: "github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository" - with: - ref: ${{ github.event.pull_request.head.sha }} - lfs: false - - # --------------------------------------------------------------------- # - # 2-d. Activate Unity license + # 2-b. Activate Unity license # --------------------------------------------------------------------- # - name: Activate Unity license id: activate @@ -78,7 +55,7 @@ jobs: unity-password: ${{ secrets.UNITY_PASSWORD || 'ZUq3YR6qM1' }} # --------------------------------------------------------------------- # - # 2-e. Cache & run the Unity test-runner + # 2-c. Cache & run the Unity test-runner # --------------------------------------------------------------------- # - name: Generate cache key id: cache_key From 3adcfdd3eb79e9635e00f4e4c92e0aa3eaaf0414 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 11 Mar 2026 01:43:14 -0700 Subject: [PATCH 63/63] fix: enhance Unity license activation logic and specify unity-license-activate version --- .github/actions/setup-unity-mcp/action.yml | 15 +++++++++++---- .github/actions/unity/activate-license/action.yml | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-unity-mcp/action.yml b/.github/actions/setup-unity-mcp/action.yml index b825b8220..ee72f05b6 100644 --- a/.github/actions/setup-unity-mcp/action.yml +++ b/.github/actions/setup-unity-mcp/action.yml @@ -82,15 +82,22 @@ runs: # --------------------------------------------------------------------- # # License activation: use provided license or activate from credentials # --------------------------------------------------------------------- # - - name: Activate Unity license - if: inputs.unity-license == '' - id: activate + - name: Activate Unity license (with credentials) + if: inputs.unity-license == '' && inputs.unity-email != '' && inputs.unity-password != '' + id: activate-creds uses: ./.github/actions/unity/activate-license with: unityVersion: ${{ steps.unity-version.outputs.unity_version }} unity-email: ${{ inputs.unity-email }} unity-password: ${{ inputs.unity-password }} + - name: Activate Unity license (with defaults) + if: inputs.unity-license == '' && (inputs.unity-email == '' || inputs.unity-password == '') + id: activate-defaults + uses: ./.github/actions/unity/activate-license + with: + unityVersion: ${{ steps.unity-version.outputs.unity_version }} + - name: Write Unity license file run: | mkdir -p "${GITHUB_WORKSPACE}/.unity-license" @@ -103,7 +110,7 @@ runs: fi env: LICENSE_FROM_INPUT: ${{ inputs.unity-license }} - LICENSE_B64: ${{ steps.activate.outputs.license }} + LICENSE_B64: ${{ steps.activate-creds.outputs.license || steps.activate-defaults.outputs.license }} shell: bash - name: Start Unity Editor in background diff --git a/.github/actions/unity/activate-license/action.yml b/.github/actions/unity/activate-license/action.yml index d05559687..b96ee1760 100644 --- a/.github/actions/unity/activate-license/action.yml +++ b/.github/actions/unity/activate-license/action.yml @@ -36,7 +36,7 @@ runs: node-version: "20" - name: Install unity-license-activate - run: npm install --global unity-license-activate + run: npm install --global unity-license-activate@0.3.9 shell: bash - name: Generate Unity activation file (.alf)