Repository: CoplayDev/unity-mcp
Branch: beta
Commit: ec25df8f791e
Files: 1717
Total size: 6.8 MB
Directory structure:
gitextract_6xex_klh/
├── .dockerignore
├── .github/
│ ├── actions/
│ │ ├── publish-docker/
│ │ │ └── action.yml
│ │ └── publish-pypi/
│ │ └── action.yml
│ ├── pull_request_template.md
│ ├── scripts/
│ │ └── mark_skipped.py
│ └── workflows/
│ ├── beta-release.yml
│ ├── claude-nl-suite.yml
│ ├── github-repo-stats.yml
│ ├── python-tests.yml
│ ├── release.yml
│ └── unity-tests.yml
├── .gitignore
├── .mcpbignore
├── CLAUDE.md
├── CustomTools/
│ └── RoslynRuntimeCompilation/
│ ├── ManageRuntimeCompilation.cs
│ ├── ManageRuntimeCompilation.cs.meta
│ ├── RoslynRuntimeCompiler.cs
│ └── RoslynRuntimeCompiler.cs.meta
├── LICENSE
├── MCPForUnity/
│ ├── Editor/
│ │ ├── AssemblyInfo.cs
│ │ ├── AssemblyInfo.cs.meta
│ │ ├── Clients/
│ │ │ ├── Configurators/
│ │ │ │ ├── AntigravityConfigurator.cs
│ │ │ │ ├── AntigravityConfigurator.cs.meta
│ │ │ │ ├── CherryStudioConfigurator.cs
│ │ │ │ ├── CherryStudioConfigurator.cs.meta
│ │ │ │ ├── ClaudeCodeConfigurator.cs
│ │ │ │ ├── ClaudeCodeConfigurator.cs.meta
│ │ │ │ ├── ClaudeDesktopConfigurator.cs
│ │ │ │ ├── ClaudeDesktopConfigurator.cs.meta
│ │ │ │ ├── ClineConfigurator.cs
│ │ │ │ ├── ClineConfigurator.cs.meta
│ │ │ │ ├── CodeBuddyCliConfigurator.cs
│ │ │ │ ├── CodeBuddyCliConfigurator.cs.meta
│ │ │ │ ├── CodexConfigurator.cs
│ │ │ │ ├── CodexConfigurator.cs.meta
│ │ │ │ ├── CopilotCliConfigurator.cs
│ │ │ │ ├── CopilotCliConfigurator.cs.meta
│ │ │ │ ├── CursorConfigurator.cs
│ │ │ │ ├── CursorConfigurator.cs.meta
│ │ │ │ ├── GeminiCliConfigurator.cs
│ │ │ │ ├── GeminiCliConfigurator.cs.meta
│ │ │ │ ├── KiloCodeConfigurator.cs
│ │ │ │ ├── KiloCodeConfigurator.cs.meta
│ │ │ │ ├── KiroConfigurator.cs
│ │ │ │ ├── KiroConfigurator.cs.meta
│ │ │ │ ├── OpenCodeConfigurator.cs
│ │ │ │ ├── OpenCodeConfigurator.cs.meta
│ │ │ │ ├── QwenCodeConfigurator.cs
│ │ │ │ ├── QwenCodeConfigurator.cs.meta
│ │ │ │ ├── RiderConfigurator.cs
│ │ │ │ ├── RiderConfigurator.cs.meta
│ │ │ │ ├── TraeConfigurator.cs
│ │ │ │ ├── TraeConfigurator.cs.meta
│ │ │ │ ├── VSCodeConfigurator.cs
│ │ │ │ ├── VSCodeConfigurator.cs.meta
│ │ │ │ ├── VSCodeInsidersConfigurator.cs
│ │ │ │ ├── VSCodeInsidersConfigurator.cs.meta
│ │ │ │ ├── WindsurfConfigurator.cs
│ │ │ │ └── WindsurfConfigurator.cs.meta
│ │ │ ├── Configurators.meta
│ │ │ ├── IMcpClientConfigurator.cs
│ │ │ ├── IMcpClientConfigurator.cs.meta
│ │ │ ├── McpClientConfiguratorBase.cs
│ │ │ ├── McpClientConfiguratorBase.cs.meta
│ │ │ ├── McpClientRegistry.cs
│ │ │ └── McpClientRegistry.cs.meta
│ │ ├── Clients.meta
│ │ ├── Constants/
│ │ │ ├── AuthConstants.cs
│ │ │ ├── AuthConstants.cs.meta
│ │ │ ├── EditorPrefKeys.cs
│ │ │ ├── EditorPrefKeys.cs.meta
│ │ │ ├── HealthStatus.cs
│ │ │ └── HealthStatus.cs.meta
│ │ ├── Constants.meta
│ │ ├── Dependencies/
│ │ │ ├── DependencyManager.cs
│ │ │ ├── DependencyManager.cs.meta
│ │ │ ├── Models/
│ │ │ │ ├── DependencyCheckResult.cs
│ │ │ │ ├── DependencyCheckResult.cs.meta
│ │ │ │ ├── DependencyStatus.cs
│ │ │ │ └── DependencyStatus.cs.meta
│ │ │ ├── Models.meta
│ │ │ ├── PlatformDetectors/
│ │ │ │ ├── IPlatformDetector.cs
│ │ │ │ ├── IPlatformDetector.cs.meta
│ │ │ │ ├── LinuxPlatformDetector.cs
│ │ │ │ ├── LinuxPlatformDetector.cs.meta
│ │ │ │ ├── MacOSPlatformDetector.cs
│ │ │ │ ├── MacOSPlatformDetector.cs.meta
│ │ │ │ ├── PlatformDetectorBase.cs
│ │ │ │ ├── PlatformDetectorBase.cs.meta
│ │ │ │ ├── WindowsPlatformDetector.cs
│ │ │ │ └── WindowsPlatformDetector.cs.meta
│ │ │ └── PlatformDetectors.meta
│ │ ├── Dependencies.meta
│ │ ├── External/
│ │ │ ├── Tommy.cs
│ │ │ └── Tommy.cs.meta
│ │ ├── External.meta
│ │ ├── Helpers/
│ │ │ ├── AssetPathUtility.cs
│ │ │ ├── AssetPathUtility.cs.meta
│ │ │ ├── CodexConfigHelper.cs
│ │ │ ├── CodexConfigHelper.cs.meta
│ │ │ ├── ComponentOps.cs
│ │ │ ├── ComponentOps.cs.meta
│ │ │ ├── ConfigJsonBuilder.cs
│ │ │ ├── ConfigJsonBuilder.cs.meta
│ │ │ ├── EditorWindowScreenshotUtility.cs
│ │ │ ├── EditorWindowScreenshotUtility.cs.meta
│ │ │ ├── ExecPath.cs
│ │ │ ├── ExecPath.cs.meta
│ │ │ ├── GameObjectLookup.cs
│ │ │ ├── GameObjectLookup.cs.meta
│ │ │ ├── GameObjectSerializer.cs
│ │ │ ├── GameObjectSerializer.cs.meta
│ │ │ ├── HttpEndpointUtility.cs
│ │ │ ├── HttpEndpointUtility.cs.meta
│ │ │ ├── MaterialOps.cs
│ │ │ ├── MaterialOps.cs.meta
│ │ │ ├── McpConfigurationHelper.cs
│ │ │ ├── McpConfigurationHelper.cs.meta
│ │ │ ├── McpJobStateStore.cs
│ │ │ ├── McpJobStateStore.cs.meta
│ │ │ ├── McpLog.cs
│ │ │ ├── McpLog.cs.meta
│ │ │ ├── McpLogRecord.cs
│ │ │ ├── McpLogRecord.cs.meta
│ │ │ ├── ObjectResolver.cs
│ │ │ ├── ObjectResolver.cs.meta
│ │ │ ├── Pagination.cs
│ │ │ ├── Pagination.cs.meta
│ │ │ ├── ParamCoercion.cs
│ │ │ ├── ParamCoercion.cs.meta
│ │ │ ├── PortManager.cs
│ │ │ ├── PortManager.cs.meta
│ │ │ ├── PrefabUtilityHelper.cs
│ │ │ ├── PrefabUtilityHelper.cs.meta
│ │ │ ├── ProjectIdentityUtility.cs
│ │ │ ├── ProjectIdentityUtility.cs.meta
│ │ │ ├── PropertyConversion.cs
│ │ │ ├── PropertyConversion.cs.meta
│ │ │ ├── RenderPipelineUtility.cs
│ │ │ ├── RenderPipelineUtility.cs.meta
│ │ │ ├── RendererHelpers.cs
│ │ │ ├── RendererHelpers.cs.meta
│ │ │ ├── Response.cs
│ │ │ ├── Response.cs.meta
│ │ │ ├── StringCaseUtility.cs
│ │ │ ├── StringCaseUtility.cs.meta
│ │ │ ├── TelemetryHelper.cs
│ │ │ ├── TelemetryHelper.cs.meta
│ │ │ ├── TextureOps.cs
│ │ │ ├── TextureOps.cs.meta
│ │ │ ├── ToolParams.cs
│ │ │ ├── ToolParams.cs.meta
│ │ │ ├── UnityJsonSerializer.cs
│ │ │ ├── UnityJsonSerializer.cs.meta
│ │ │ ├── UnityTypeResolver.cs
│ │ │ ├── UnityTypeResolver.cs.meta
│ │ │ ├── VectorParsing.cs
│ │ │ └── VectorParsing.cs.meta
│ │ ├── Helpers.meta
│ │ ├── MCPForUnity.Editor.asmdef
│ │ ├── MCPForUnity.Editor.asmdef.meta
│ │ ├── McpCiBoot.cs
│ │ ├── McpCiBoot.cs.meta
│ │ ├── MenuItems/
│ │ │ ├── MCPForUnityMenu.cs
│ │ │ └── MCPForUnityMenu.cs.meta
│ │ ├── MenuItems.meta
│ │ ├── Migrations/
│ │ │ ├── LegacyServerSrcMigration.cs
│ │ │ ├── LegacyServerSrcMigration.cs.meta
│ │ │ ├── StdIoVersionMigration.cs
│ │ │ └── StdIoVersionMigration.cs.meta
│ │ ├── Migrations.meta
│ │ ├── Models/
│ │ │ ├── Command.cs
│ │ │ ├── Command.cs.meta
│ │ │ ├── MCPConfigServer.cs
│ │ │ ├── MCPConfigServer.cs.meta
│ │ │ ├── MCPConfigServers.cs
│ │ │ ├── MCPConfigServers.cs.meta
│ │ │ ├── McpClient.cs
│ │ │ ├── McpClient.cs.meta
│ │ │ ├── McpConfig.cs
│ │ │ ├── McpConfig.cs.meta
│ │ │ ├── McpStatus.cs
│ │ │ └── McpStatus.cs.meta
│ │ ├── Models.meta
│ │ ├── Resources/
│ │ │ ├── Editor/
│ │ │ │ ├── ActiveTool.cs
│ │ │ │ ├── ActiveTool.cs.meta
│ │ │ │ ├── EditorState.cs
│ │ │ │ ├── EditorState.cs.meta
│ │ │ │ ├── Selection.cs
│ │ │ │ ├── Selection.cs.meta
│ │ │ │ ├── ToolStates.cs
│ │ │ │ ├── ToolStates.cs.meta
│ │ │ │ ├── Windows.cs
│ │ │ │ └── Windows.cs.meta
│ │ │ ├── Editor.meta
│ │ │ ├── McpForUnityResourceAttribute.cs
│ │ │ ├── McpForUnityResourceAttribute.cs.meta
│ │ │ ├── MenuItems/
│ │ │ │ ├── GetMenuItems.cs
│ │ │ │ └── GetMenuItems.cs.meta
│ │ │ ├── MenuItems.meta
│ │ │ ├── Project/
│ │ │ │ ├── Layers.cs
│ │ │ │ ├── Layers.cs.meta
│ │ │ │ ├── ProjectInfo.cs
│ │ │ │ ├── ProjectInfo.cs.meta
│ │ │ │ ├── Tags.cs
│ │ │ │ └── Tags.cs.meta
│ │ │ ├── Project.meta
│ │ │ ├── Scene/
│ │ │ │ ├── CamerasResource.cs
│ │ │ │ ├── CamerasResource.cs.meta
│ │ │ │ ├── GameObjectResource.cs
│ │ │ │ ├── GameObjectResource.cs.meta
│ │ │ │ ├── RendererFeaturesResource.cs
│ │ │ │ ├── RendererFeaturesResource.cs.meta
│ │ │ │ ├── RenderingStatsResource.cs
│ │ │ │ ├── RenderingStatsResource.cs.meta
│ │ │ │ ├── VolumesResource.cs
│ │ │ │ └── VolumesResource.cs.meta
│ │ │ ├── Scene.meta
│ │ │ ├── Tests/
│ │ │ │ ├── GetTests.cs
│ │ │ │ └── GetTests.cs.meta
│ │ │ └── Tests.meta
│ │ ├── Resources.meta
│ │ ├── Services/
│ │ │ ├── BridgeControlService.cs
│ │ │ ├── BridgeControlService.cs.meta
│ │ │ ├── ClientConfigurationService.cs
│ │ │ ├── ClientConfigurationService.cs.meta
│ │ │ ├── EditorConfigurationCache.cs
│ │ │ ├── EditorConfigurationCache.cs.meta
│ │ │ ├── EditorPrefsWindowService.cs
│ │ │ ├── EditorPrefsWindowService.cs.meta
│ │ │ ├── EditorStateCache.cs
│ │ │ ├── EditorStateCache.cs.meta
│ │ │ ├── HttpAutoStartHandler.cs
│ │ │ ├── HttpAutoStartHandler.cs.meta
│ │ │ ├── HttpBridgeReloadHandler.cs
│ │ │ ├── HttpBridgeReloadHandler.cs.meta
│ │ │ ├── IBridgeControlService.cs
│ │ │ ├── IBridgeControlService.cs.meta
│ │ │ ├── IClientConfigurationService.cs
│ │ │ ├── IClientConfigurationService.cs.meta
│ │ │ ├── IPackageDeploymentService.cs
│ │ │ ├── IPackageDeploymentService.cs.meta
│ │ │ ├── IPackageUpdateService.cs
│ │ │ ├── IPackageUpdateService.cs.meta
│ │ │ ├── IPathResolverService.cs
│ │ │ ├── IPathResolverService.cs.meta
│ │ │ ├── IPlatformService.cs
│ │ │ ├── IPlatformService.cs.meta
│ │ │ ├── IResourceDiscoveryService.cs
│ │ │ ├── IResourceDiscoveryService.cs.meta
│ │ │ ├── IServerManagementService.cs
│ │ │ ├── IServerManagementService.cs.meta
│ │ │ ├── ITestRunnerService.cs
│ │ │ ├── ITestRunnerService.cs.meta
│ │ │ ├── IToolDiscoveryService.cs
│ │ │ ├── IToolDiscoveryService.cs.meta
│ │ │ ├── MCPServiceLocator.cs
│ │ │ ├── MCPServiceLocator.cs.meta
│ │ │ ├── McpEditorShutdownCleanup.cs
│ │ │ ├── McpEditorShutdownCleanup.cs.meta
│ │ │ ├── PackageDeploymentService.cs
│ │ │ ├── PackageDeploymentService.cs.meta
│ │ │ ├── PackageJobManager.cs
│ │ │ ├── PackageJobManager.cs.meta
│ │ │ ├── PackageUpdateService.cs
│ │ │ ├── PackageUpdateService.cs.meta
│ │ │ ├── PathResolverService.cs
│ │ │ ├── PathResolverService.cs.meta
│ │ │ ├── PlatformService.cs
│ │ │ ├── PlatformService.cs.meta
│ │ │ ├── ResourceDiscoveryService.cs
│ │ │ ├── ResourceDiscoveryService.cs.meta
│ │ │ ├── Server/
│ │ │ │ ├── IPidFileManager.cs
│ │ │ │ ├── IPidFileManager.cs.meta
│ │ │ │ ├── IProcessDetector.cs
│ │ │ │ ├── IProcessDetector.cs.meta
│ │ │ │ ├── IProcessTerminator.cs
│ │ │ │ ├── IProcessTerminator.cs.meta
│ │ │ │ ├── IServerCommandBuilder.cs
│ │ │ │ ├── IServerCommandBuilder.cs.meta
│ │ │ │ ├── ITerminalLauncher.cs
│ │ │ │ ├── ITerminalLauncher.cs.meta
│ │ │ │ ├── PidFileManager.cs
│ │ │ │ ├── PidFileManager.cs.meta
│ │ │ │ ├── ProcessDetector.cs
│ │ │ │ ├── ProcessDetector.cs.meta
│ │ │ │ ├── ProcessTerminator.cs
│ │ │ │ ├── ProcessTerminator.cs.meta
│ │ │ │ ├── ServerCommandBuilder.cs
│ │ │ │ ├── ServerCommandBuilder.cs.meta
│ │ │ │ ├── TerminalLauncher.cs
│ │ │ │ └── TerminalLauncher.cs.meta
│ │ │ ├── Server.meta
│ │ │ ├── ServerManagementService.cs
│ │ │ ├── ServerManagementService.cs.meta
│ │ │ ├── StdioBridgeReloadHandler.cs
│ │ │ ├── StdioBridgeReloadHandler.cs.meta
│ │ │ ├── TestJobManager.cs
│ │ │ ├── TestJobManager.cs.meta
│ │ │ ├── TestRunStatus.cs
│ │ │ ├── TestRunStatus.cs.meta
│ │ │ ├── TestRunnerNoThrottle.cs
│ │ │ ├── TestRunnerNoThrottle.cs.meta
│ │ │ ├── TestRunnerService.cs
│ │ │ ├── TestRunnerService.cs.meta
│ │ │ ├── ToolDiscoveryService.cs
│ │ │ ├── ToolDiscoveryService.cs.meta
│ │ │ ├── Transport/
│ │ │ │ ├── IMcpTransportClient.cs
│ │ │ │ ├── IMcpTransportClient.cs.meta
│ │ │ │ ├── TransportCommandDispatcher.cs
│ │ │ │ ├── TransportCommandDispatcher.cs.meta
│ │ │ │ ├── TransportManager.cs
│ │ │ │ ├── TransportManager.cs.meta
│ │ │ │ ├── TransportState.cs
│ │ │ │ ├── TransportState.cs.meta
│ │ │ │ ├── Transports/
│ │ │ │ │ ├── StdioBridgeHost.cs
│ │ │ │ │ ├── StdioBridgeHost.cs.meta
│ │ │ │ │ ├── StdioTransportClient.cs
│ │ │ │ │ ├── StdioTransportClient.cs.meta
│ │ │ │ │ ├── WebSocketTransportClient.cs
│ │ │ │ │ └── WebSocketTransportClient.cs.meta
│ │ │ │ └── Transports.meta
│ │ │ └── Transport.meta
│ │ ├── Services.meta
│ │ ├── Setup/
│ │ │ ├── McpForUnitySkillInstaller.cs
│ │ │ ├── McpForUnitySkillInstaller.cs.meta
│ │ │ ├── RoslynInstaller.cs
│ │ │ ├── RoslynInstaller.cs.meta
│ │ │ ├── SetupWindowService.cs
│ │ │ ├── SetupWindowService.cs.meta
│ │ │ ├── SkillSyncService.cs
│ │ │ └── SkillSyncService.cs.meta
│ │ ├── Setup.meta
│ │ ├── Tools/
│ │ │ ├── Animation/
│ │ │ │ ├── AnimatorControl.cs
│ │ │ │ ├── AnimatorControl.cs.meta
│ │ │ │ ├── AnimatorRead.cs
│ │ │ │ ├── AnimatorRead.cs.meta
│ │ │ │ ├── ClipCreate.cs
│ │ │ │ ├── ClipCreate.cs.meta
│ │ │ │ ├── ClipPresets.cs
│ │ │ │ ├── ClipPresets.cs.meta
│ │ │ │ ├── ControllerBlendTrees.cs
│ │ │ │ ├── ControllerBlendTrees.cs.meta
│ │ │ │ ├── ControllerCreate.cs
│ │ │ │ ├── ControllerCreate.cs.meta
│ │ │ │ ├── ControllerLayers.cs
│ │ │ │ ├── ControllerLayers.cs.meta
│ │ │ │ ├── ManageAnimation.cs
│ │ │ │ └── ManageAnimation.cs.meta
│ │ │ ├── Animation.meta
│ │ │ ├── BatchExecute.cs
│ │ │ ├── BatchExecute.cs.meta
│ │ │ ├── Cameras/
│ │ │ │ ├── CameraConfigure.cs
│ │ │ │ ├── CameraConfigure.cs.meta
│ │ │ │ ├── CameraControl.cs
│ │ │ │ ├── CameraControl.cs.meta
│ │ │ │ ├── CameraCreate.cs
│ │ │ │ ├── CameraCreate.cs.meta
│ │ │ │ ├── CameraHelpers.cs
│ │ │ │ ├── CameraHelpers.cs.meta
│ │ │ │ ├── ManageCamera.cs
│ │ │ │ └── ManageCamera.cs.meta
│ │ │ ├── Cameras.meta
│ │ │ ├── CommandRegistry.cs
│ │ │ ├── CommandRegistry.cs.meta
│ │ │ ├── ExecuteMenuItem.cs
│ │ │ ├── ExecuteMenuItem.cs.meta
│ │ │ ├── FindGameObjects.cs
│ │ │ ├── FindGameObjects.cs.meta
│ │ │ ├── GameObjects/
│ │ │ │ ├── ComponentResolver.cs
│ │ │ │ ├── ComponentResolver.cs.meta
│ │ │ │ ├── GameObjectComponentHelpers.cs
│ │ │ │ ├── GameObjectComponentHelpers.cs.meta
│ │ │ │ ├── GameObjectCreate.cs
│ │ │ │ ├── GameObjectCreate.cs.meta
│ │ │ │ ├── GameObjectDelete.cs
│ │ │ │ ├── GameObjectDelete.cs.meta
│ │ │ │ ├── GameObjectDuplicate.cs
│ │ │ │ ├── GameObjectDuplicate.cs.meta
│ │ │ │ ├── GameObjectHandlers.cs
│ │ │ │ ├── GameObjectHandlers.cs.meta
│ │ │ │ ├── GameObjectLookAt.cs
│ │ │ │ ├── GameObjectLookAt.cs.meta
│ │ │ │ ├── GameObjectModify.cs
│ │ │ │ ├── GameObjectModify.cs.meta
│ │ │ │ ├── GameObjectMoveRelative.cs
│ │ │ │ ├── GameObjectMoveRelative.cs.meta
│ │ │ │ ├── ManageGameObject.cs
│ │ │ │ ├── ManageGameObject.cs.meta
│ │ │ │ ├── ManageGameObjectCommon.cs
│ │ │ │ └── ManageGameObjectCommon.cs.meta
│ │ │ ├── GameObjects.meta
│ │ │ ├── GetTestJob.cs
│ │ │ ├── GetTestJob.cs.meta
│ │ │ ├── Graphics/
│ │ │ │ ├── GraphicsHelpers.cs
│ │ │ │ ├── GraphicsHelpers.cs.meta
│ │ │ │ ├── LightBakingOps.cs
│ │ │ │ ├── LightBakingOps.cs.meta
│ │ │ │ ├── ManageGraphics.cs
│ │ │ │ ├── ManageGraphics.cs.meta
│ │ │ │ ├── RenderPipelineOps.cs
│ │ │ │ ├── RenderPipelineOps.cs.meta
│ │ │ │ ├── RendererFeatureOps.cs
│ │ │ │ ├── RendererFeatureOps.cs.meta
│ │ │ │ ├── RenderingStatsOps.cs
│ │ │ │ ├── RenderingStatsOps.cs.meta
│ │ │ │ ├── SkyboxOps.cs
│ │ │ │ ├── SkyboxOps.cs.meta
│ │ │ │ ├── VolumeOps.cs
│ │ │ │ └── VolumeOps.cs.meta
│ │ │ ├── Graphics.meta
│ │ │ ├── JsonUtil.cs
│ │ │ ├── JsonUtil.cs.meta
│ │ │ ├── ManageAsset.cs
│ │ │ ├── ManageAsset.cs.meta
│ │ │ ├── ManageComponents.cs
│ │ │ ├── ManageComponents.cs.meta
│ │ │ ├── ManageEditor.cs
│ │ │ ├── ManageEditor.cs.meta
│ │ │ ├── ManageMaterial.cs
│ │ │ ├── ManageMaterial.cs.meta
│ │ │ ├── ManagePackages.cs
│ │ │ ├── ManagePackages.cs.meta
│ │ │ ├── ManageScene.cs
│ │ │ ├── ManageScene.cs.meta
│ │ │ ├── ManageScript.cs
│ │ │ ├── ManageScript.cs.meta
│ │ │ ├── ManageScriptableObject.cs
│ │ │ ├── ManageScriptableObject.cs.meta
│ │ │ ├── ManageShader.cs
│ │ │ ├── ManageShader.cs.meta
│ │ │ ├── ManageTexture.cs
│ │ │ ├── ManageTexture.cs.meta
│ │ │ ├── ManageUI.cs
│ │ │ ├── ManageUI.cs.meta
│ │ │ ├── McpForUnityToolAttribute.cs
│ │ │ ├── McpForUnityToolAttribute.cs.meta
│ │ │ ├── Prefabs/
│ │ │ │ ├── ManagePrefabs.cs
│ │ │ │ └── ManagePrefabs.cs.meta
│ │ │ ├── Prefabs.meta
│ │ │ ├── ProBuilder/
│ │ │ │ ├── ManageProBuilder.cs
│ │ │ │ ├── ManageProBuilder.cs.meta
│ │ │ │ ├── ProBuilderMeshUtils.cs
│ │ │ │ ├── ProBuilderMeshUtils.cs.meta
│ │ │ │ ├── ProBuilderSmoothing.cs
│ │ │ │ └── ProBuilderSmoothing.cs.meta
│ │ │ ├── ProBuilder.meta
│ │ │ ├── ReadConsole.cs
│ │ │ ├── ReadConsole.cs.meta
│ │ │ ├── RefreshUnity.cs
│ │ │ ├── RefreshUnity.cs.meta
│ │ │ ├── RunTests.cs
│ │ │ ├── RunTests.cs.meta
│ │ │ ├── UnityReflect.cs
│ │ │ ├── UnityReflect.cs.meta
│ │ │ ├── Vfx/
│ │ │ │ ├── LineCreate.cs
│ │ │ │ ├── LineCreate.cs.meta
│ │ │ │ ├── LineRead.cs
│ │ │ │ ├── LineRead.cs.meta
│ │ │ │ ├── LineWrite.cs
│ │ │ │ ├── LineWrite.cs.meta
│ │ │ │ ├── ManageVFX.cs
│ │ │ │ ├── ManageVFX.cs.meta
│ │ │ │ ├── ManageVfxCommon.cs
│ │ │ │ ├── ManageVfxCommon.cs.meta
│ │ │ │ ├── ParticleCommon.cs
│ │ │ │ ├── ParticleCommon.cs.meta
│ │ │ │ ├── ParticleControl.cs
│ │ │ │ ├── ParticleControl.cs.meta
│ │ │ │ ├── ParticleRead.cs
│ │ │ │ ├── ParticleRead.cs.meta
│ │ │ │ ├── ParticleWrite.cs
│ │ │ │ ├── ParticleWrite.cs.meta
│ │ │ │ ├── TrailControl.cs
│ │ │ │ ├── TrailControl.cs.meta
│ │ │ │ ├── TrailRead.cs
│ │ │ │ ├── TrailRead.cs.meta
│ │ │ │ ├── TrailWrite.cs
│ │ │ │ ├── TrailWrite.cs.meta
│ │ │ │ ├── VfxGraphAssets.cs
│ │ │ │ ├── VfxGraphAssets.cs.meta
│ │ │ │ ├── VfxGraphCommon.cs
│ │ │ │ ├── VfxGraphCommon.cs.meta
│ │ │ │ ├── VfxGraphControl.cs
│ │ │ │ ├── VfxGraphControl.cs.meta
│ │ │ │ ├── VfxGraphRead.cs
│ │ │ │ ├── VfxGraphRead.cs.meta
│ │ │ │ ├── VfxGraphWrite.cs
│ │ │ │ └── VfxGraphWrite.cs.meta
│ │ │ └── Vfx.meta
│ │ ├── Tools.meta
│ │ ├── Windows/
│ │ │ ├── Components/
│ │ │ │ ├── Advanced/
│ │ │ │ │ ├── McpAdvancedSection.cs
│ │ │ │ │ ├── McpAdvancedSection.cs.meta
│ │ │ │ │ ├── McpAdvancedSection.uxml
│ │ │ │ │ └── McpAdvancedSection.uxml.meta
│ │ │ │ ├── Advanced.meta
│ │ │ │ ├── ClientConfig/
│ │ │ │ │ ├── McpClientConfigSection.cs
│ │ │ │ │ ├── McpClientConfigSection.cs.meta
│ │ │ │ │ ├── McpClientConfigSection.uxml
│ │ │ │ │ └── McpClientConfigSection.uxml.meta
│ │ │ │ ├── ClientConfig.meta
│ │ │ │ ├── Common.uss
│ │ │ │ ├── Common.uss.meta
│ │ │ │ ├── Connection/
│ │ │ │ │ ├── McpConnectionSection.cs
│ │ │ │ │ ├── McpConnectionSection.cs.meta
│ │ │ │ │ ├── McpConnectionSection.uxml
│ │ │ │ │ └── McpConnectionSection.uxml.meta
│ │ │ │ ├── Connection.meta
│ │ │ │ ├── Resources/
│ │ │ │ │ ├── McpResourcesSection.cs
│ │ │ │ │ ├── McpResourcesSection.cs.meta
│ │ │ │ │ ├── McpResourcesSection.uxml
│ │ │ │ │ └── McpResourcesSection.uxml.meta
│ │ │ │ ├── Resources.meta
│ │ │ │ ├── Tools/
│ │ │ │ │ ├── McpToolsSection.cs
│ │ │ │ │ ├── McpToolsSection.cs.meta
│ │ │ │ │ ├── McpToolsSection.uxml
│ │ │ │ │ └── McpToolsSection.uxml.meta
│ │ │ │ ├── Tools.meta
│ │ │ │ ├── Validation/
│ │ │ │ │ ├── McpValidationSection.cs
│ │ │ │ │ ├── McpValidationSection.cs.meta
│ │ │ │ │ ├── McpValidationSection.uxml
│ │ │ │ │ └── McpValidationSection.uxml.meta
│ │ │ │ └── Validation.meta
│ │ │ ├── Components.meta
│ │ │ ├── EditorPrefs/
│ │ │ │ ├── EditorPrefItem.uxml
│ │ │ │ ├── EditorPrefItem.uxml.meta
│ │ │ │ ├── EditorPrefsWindow.cs
│ │ │ │ ├── EditorPrefsWindow.cs.meta
│ │ │ │ ├── EditorPrefsWindow.uss
│ │ │ │ ├── EditorPrefsWindow.uss.meta
│ │ │ │ ├── EditorPrefsWindow.uxml
│ │ │ │ └── EditorPrefsWindow.uxml.meta
│ │ │ ├── EditorPrefs.meta
│ │ │ ├── MCPForUnityEditorWindow.cs
│ │ │ ├── MCPForUnityEditorWindow.cs.meta
│ │ │ ├── MCPForUnityEditorWindow.uss
│ │ │ ├── MCPForUnityEditorWindow.uss.meta
│ │ │ ├── MCPForUnityEditorWindow.uxml
│ │ │ ├── MCPForUnityEditorWindow.uxml.meta
│ │ │ ├── MCPSetupWindow.cs
│ │ │ ├── MCPSetupWindow.cs.meta
│ │ │ ├── MCPSetupWindow.uss
│ │ │ ├── MCPSetupWindow.uss.meta
│ │ │ ├── MCPSetupWindow.uxml
│ │ │ └── MCPSetupWindow.uxml.meta
│ │ └── Windows.meta
│ ├── Editor.meta
│ ├── README.md
│ ├── README.md.meta
│ ├── Runtime/
│ │ ├── Helpers/
│ │ │ ├── ScreenshotUtility.cs
│ │ │ └── ScreenshotUtility.cs.meta
│ │ ├── Helpers.meta
│ │ ├── MCPForUnity.Runtime.asmdef
│ │ ├── MCPForUnity.Runtime.asmdef.meta
│ │ ├── Serialization/
│ │ │ ├── UnityTypeConverters.cs
│ │ │ └── UnityTypeConverters.cs.meta
│ │ └── Serialization.meta
│ ├── Runtime.meta
│ ├── package.json
│ └── package.json.meta
├── README.md
├── Server/
│ ├── DOCKER_OVERVIEW.md
│ ├── Dockerfile
│ ├── LICENSE
│ ├── README.md
│ ├── __init__.py
│ ├── pyproject.toml
│ ├── pyrightconfig.json
│ ├── src/
│ │ ├── __init__.py
│ │ ├── cli/
│ │ │ ├── CLI_USAGE_GUIDE.md
│ │ │ ├── __init__.py
│ │ │ ├── commands/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── animation.py
│ │ │ │ ├── asset.py
│ │ │ │ ├── audio.py
│ │ │ │ ├── batch.py
│ │ │ │ ├── camera.py
│ │ │ │ ├── code.py
│ │ │ │ ├── component.py
│ │ │ │ ├── docs.py
│ │ │ │ ├── editor.py
│ │ │ │ ├── gameobject.py
│ │ │ │ ├── graphics.py
│ │ │ │ ├── instance.py
│ │ │ │ ├── lighting.py
│ │ │ │ ├── material.py
│ │ │ │ ├── packages.py
│ │ │ │ ├── prefab.py
│ │ │ │ ├── probuilder.py
│ │ │ │ ├── reflect.py
│ │ │ │ ├── scene.py
│ │ │ │ ├── script.py
│ │ │ │ ├── shader.py
│ │ │ │ ├── texture.py
│ │ │ │ ├── tool.py
│ │ │ │ ├── ui.py
│ │ │ │ └── vfx.py
│ │ │ ├── main.py
│ │ │ └── utils/
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── confirmation.py
│ │ │ ├── connection.py
│ │ │ ├── constants.py
│ │ │ ├── output.py
│ │ │ ├── parsers.py
│ │ │ └── suggestions.py
│ │ ├── core/
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── constants.py
│ │ │ ├── logging_decorator.py
│ │ │ ├── telemetry.py
│ │ │ └── telemetry_decorator.py
│ │ ├── main.py
│ │ ├── models/
│ │ │ ├── __init__.py
│ │ │ ├── models.py
│ │ │ └── unity_response.py
│ │ ├── services/
│ │ │ ├── __init__.py
│ │ │ ├── api_key_service.py
│ │ │ ├── custom_tool_service.py
│ │ │ ├── registry/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── resource_registry.py
│ │ │ │ └── tool_registry.py
│ │ │ ├── resources/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── active_tool.py
│ │ │ │ ├── cameras.py
│ │ │ │ ├── custom_tools.py
│ │ │ │ ├── editor_state.py
│ │ │ │ ├── gameobject.py
│ │ │ │ ├── layers.py
│ │ │ │ ├── menu_items.py
│ │ │ │ ├── prefab.py
│ │ │ │ ├── prefab_stage.py
│ │ │ │ ├── project_info.py
│ │ │ │ ├── renderer_features.py
│ │ │ │ ├── rendering_stats.py
│ │ │ │ ├── selection.py
│ │ │ │ ├── tags.py
│ │ │ │ ├── tests.py
│ │ │ │ ├── tool_groups.py
│ │ │ │ ├── unity_instances.py
│ │ │ │ ├── volumes.py
│ │ │ │ └── windows.py
│ │ │ ├── state/
│ │ │ │ └── external_changes_scanner.py
│ │ │ └── tools/
│ │ │ ├── __init__.py
│ │ │ ├── batch_execute.py
│ │ │ ├── debug_request_context.py
│ │ │ ├── execute_custom_tool.py
│ │ │ ├── execute_menu_item.py
│ │ │ ├── find_gameobjects.py
│ │ │ ├── find_in_file.py
│ │ │ ├── manage_animation.py
│ │ │ ├── manage_asset.py
│ │ │ ├── manage_camera.py
│ │ │ ├── manage_components.py
│ │ │ ├── manage_editor.py
│ │ │ ├── manage_gameobject.py
│ │ │ ├── manage_graphics.py
│ │ │ ├── manage_material.py
│ │ │ ├── manage_packages.py
│ │ │ ├── manage_prefabs.py
│ │ │ ├── manage_probuilder.py
│ │ │ ├── manage_scene.py
│ │ │ ├── manage_script.py
│ │ │ ├── manage_scriptable_object.py
│ │ │ ├── manage_shader.py
│ │ │ ├── manage_texture.py
│ │ │ ├── manage_tools.py
│ │ │ ├── manage_ui.py
│ │ │ ├── manage_vfx.py
│ │ │ ├── preflight.py
│ │ │ ├── read_console.py
│ │ │ ├── refresh_unity.py
│ │ │ ├── run_tests.py
│ │ │ ├── script_apply_edits.py
│ │ │ ├── set_active_instance.py
│ │ │ ├── unity_docs.py
│ │ │ ├── unity_reflect.py
│ │ │ └── utils.py
│ │ ├── transport/
│ │ │ ├── __init__.py
│ │ │ ├── legacy/
│ │ │ │ ├── port_discovery.py
│ │ │ │ ├── stdio_port_registry.py
│ │ │ │ └── unity_connection.py
│ │ │ ├── models.py
│ │ │ ├── plugin_hub.py
│ │ │ ├── plugin_registry.py
│ │ │ ├── unity_instance_middleware.py
│ │ │ └── unity_transport.py
│ │ └── utils/
│ │ ├── focus_nudge.py
│ │ └── module_discovery.py
│ └── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── integration/
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_api_key_service.py
│ │ ├── test_auth_config_startup.py
│ │ ├── test_debug_request_context_diagnostics.py
│ │ ├── test_domain_reload_resilience.py
│ │ ├── test_edit_normalization_and_noop.py
│ │ ├── test_edit_strict_and_warnings.py
│ │ ├── test_editor_state_v2_contract.py
│ │ ├── test_external_changes_scanner.py
│ │ ├── test_find_gameobjects.py
│ │ ├── test_gameobject_resources.py
│ │ ├── test_get_sha.py
│ │ ├── test_helpers.py
│ │ ├── test_improved_anchor_matching.py
│ │ ├── test_inline_unity_instance.py
│ │ ├── test_instance_autoselect.py
│ │ ├── test_instance_routing_comprehensive.py
│ │ ├── test_instance_targeting_resolution.py
│ │ ├── test_json_parsing_simple.py
│ │ ├── test_logging_stdout.py
│ │ ├── test_manage_asset_json_parsing.py
│ │ ├── test_manage_asset_param_coercion.py
│ │ ├── test_manage_components.py
│ │ ├── test_manage_gameobject_look_at.py
│ │ ├── test_manage_gameobject_param_coercion.py
│ │ ├── test_manage_scene_paging_params.py
│ │ ├── test_manage_script_uri.py
│ │ ├── test_manage_scriptable_object_tool.py
│ │ ├── test_manage_texture.py
│ │ ├── test_manage_ui.py
│ │ ├── test_middleware_auth_integration.py
│ │ ├── test_multi_user_session_isolation.py
│ │ ├── test_plugin_hub_websocket_auth.py
│ │ ├── test_plugin_registry_user_isolation.py
│ │ ├── test_read_console_truncate.py
│ │ ├── test_read_resource_minimal.py
│ │ ├── test_refresh_unity_registration.py
│ │ ├── test_refresh_unity_retry_recovery.py
│ │ ├── test_resolve_user_id.py
│ │ ├── test_run_tests_async.py
│ │ ├── test_script_apply_edits_local.py
│ │ ├── test_script_tools.py
│ │ ├── test_telemetry_endpoint_validation.py
│ │ ├── test_telemetry_queue_worker.py
│ │ ├── test_telemetry_subaction.py
│ │ ├── test_tool_signatures_paging.py
│ │ ├── test_transport_framing.py
│ │ ├── test_transport_smoke.py
│ │ ├── test_validate_script_summary.py
│ │ └── test_wait_for_editor_ready.py
│ ├── pytest.ini
│ ├── test_cli.py
│ ├── test_cli_commands_characterization.py
│ ├── test_core_infrastructure_characterization.py
│ ├── test_custom_tool_service_user_scope.py
│ ├── test_focus_nudge.py
│ ├── test_manage_animation.py
│ ├── test_manage_camera.py
│ ├── test_manage_graphics.py
│ ├── test_manage_prefabs.py
│ ├── test_manage_probuilder.py
│ ├── test_manage_vfx_actions.py
│ ├── test_models_characterization.py
│ ├── test_param_normalizer.py
│ ├── test_tool_registry_metadata.py
│ ├── test_transport_characterization.py
│ ├── test_unity_docs.py
│ ├── test_unity_reflect.py
│ └── test_utilities_characterization.py
├── TestProjects/
│ ├── AssetStoreUploads/
│ │ ├── .gitignore
│ │ ├── Assets/
│ │ │ ├── Readme.asset.meta
│ │ │ ├── Scenes/
│ │ │ │ ├── SampleScene.unity
│ │ │ │ └── SampleScene.unity.meta
│ │ │ ├── Scenes.meta
│ │ │ ├── Settings/
│ │ │ │ ├── SampleSceneProfile.asset.meta
│ │ │ │ ├── URP-Balanced-Renderer.asset.meta
│ │ │ │ ├── URP-Balanced.asset.meta
│ │ │ │ ├── URP-HighFidelity-Renderer.asset.meta
│ │ │ │ ├── URP-HighFidelity.asset.meta
│ │ │ │ ├── URP-Performant-Renderer.asset.meta
│ │ │ │ └── URP-Performant.asset.meta
│ │ │ ├── Settings.meta
│ │ │ ├── TutorialInfo/
│ │ │ │ ├── Icons/
│ │ │ │ │ └── URP.png.meta
│ │ │ │ ├── Icons.meta
│ │ │ │ ├── Layout.wlt
│ │ │ │ ├── Layout.wlt.meta
│ │ │ │ ├── Scripts/
│ │ │ │ │ ├── Editor/
│ │ │ │ │ │ ├── ReadmeEditor.cs
│ │ │ │ │ │ └── ReadmeEditor.cs.meta
│ │ │ │ │ ├── Editor.meta
│ │ │ │ │ ├── Readme.cs
│ │ │ │ │ └── Readme.cs.meta
│ │ │ │ └── Scripts.meta
│ │ │ ├── TutorialInfo.meta
│ │ │ └── UniversalRenderPipelineGlobalSettings.asset.meta
│ │ ├── Packages/
│ │ │ ├── com.unity.asset-store-tools/
│ │ │ │ ├── CHANGELOG.md
│ │ │ │ ├── CHANGELOG.md.meta
│ │ │ │ ├── Editor/
│ │ │ │ │ ├── Api/
│ │ │ │ │ │ ├── Abstractions/
│ │ │ │ │ │ │ ├── AuthenticationBase.cs
│ │ │ │ │ │ │ ├── AuthenticationBase.cs.meta
│ │ │ │ │ │ │ ├── IAssetStoreApi.cs
│ │ │ │ │ │ │ ├── IAssetStoreApi.cs.meta
│ │ │ │ │ │ │ ├── IAssetStoreClient.cs
│ │ │ │ │ │ │ ├── IAssetStoreClient.cs.meta
│ │ │ │ │ │ │ ├── IAuthenticationType.cs
│ │ │ │ │ │ │ ├── IAuthenticationType.cs.meta
│ │ │ │ │ │ │ ├── IPackageUploader.cs
│ │ │ │ │ │ │ ├── IPackageUploader.cs.meta
│ │ │ │ │ │ │ ├── PackageUploaderBase.cs
│ │ │ │ │ │ │ └── PackageUploaderBase.cs.meta
│ │ │ │ │ │ ├── Abstractions.meta
│ │ │ │ │ │ ├── ApiUtility.cs
│ │ │ │ │ │ ├── ApiUtility.cs.meta
│ │ │ │ │ │ ├── AssetStoreApi.cs
│ │ │ │ │ │ ├── AssetStoreApi.cs.meta
│ │ │ │ │ │ ├── AssetStoreClient.cs
│ │ │ │ │ │ ├── AssetStoreClient.cs.meta
│ │ │ │ │ │ ├── CloudTokenAuthentication.cs
│ │ │ │ │ │ ├── CloudTokenAuthentication.cs.meta
│ │ │ │ │ │ ├── CredentialsAuthentication.cs
│ │ │ │ │ │ ├── CredentialsAuthentication.cs.meta
│ │ │ │ │ │ ├── Models/
│ │ │ │ │ │ │ ├── Category.cs
│ │ │ │ │ │ │ ├── Category.cs.meta
│ │ │ │ │ │ │ ├── Package.cs
│ │ │ │ │ │ │ ├── Package.cs.meta
│ │ │ │ │ │ │ ├── PackageAdditionalData.cs
│ │ │ │ │ │ │ ├── PackageAdditionalData.cs.meta
│ │ │ │ │ │ │ ├── User.cs
│ │ │ │ │ │ │ └── User.cs.meta
│ │ │ │ │ │ ├── Models.meta
│ │ │ │ │ │ ├── Responses/
│ │ │ │ │ │ │ ├── AssetStoreResponse.cs
│ │ │ │ │ │ │ ├── AssetStoreResponse.cs.meta
│ │ │ │ │ │ │ ├── AssetStoreToolsVersionResponse.cs
│ │ │ │ │ │ │ ├── AssetStoreToolsVersionResponse.cs.meta
│ │ │ │ │ │ │ ├── AuthenticationResponse.cs
│ │ │ │ │ │ │ ├── AuthenticationResponse.cs.meta
│ │ │ │ │ │ │ ├── CategoryDataResponse.cs
│ │ │ │ │ │ │ ├── CategoryDataResponse.cs.meta
│ │ │ │ │ │ │ ├── PackageThumbnailResponse.cs
│ │ │ │ │ │ │ ├── PackageThumbnailResponse.cs.meta
│ │ │ │ │ │ │ ├── PackageUploadedUnityVersionDataResponse.cs
│ │ │ │ │ │ │ ├── PackageUploadedUnityVersionDataResponse.cs.meta
│ │ │ │ │ │ │ ├── PackagesAdditionalDataResponse.cs
│ │ │ │ │ │ │ ├── PackagesAdditionalDataResponse.cs.meta
│ │ │ │ │ │ │ ├── PackagesDataResponse.cs
│ │ │ │ │ │ │ ├── PackagesDataResponse.cs.meta
│ │ │ │ │ │ │ ├── RefreshedPackageDataResponse.cs
│ │ │ │ │ │ │ ├── RefreshedPackageDataResponse.cs.meta
│ │ │ │ │ │ │ ├── UploadResponse.cs
│ │ │ │ │ │ │ └── UploadResponse.cs.meta
│ │ │ │ │ │ ├── Responses.meta
│ │ │ │ │ │ ├── SessionAuthentication.cs
│ │ │ │ │ │ ├── SessionAuthentication.cs.meta
│ │ │ │ │ │ ├── UnityPackageUploader.cs
│ │ │ │ │ │ ├── UnityPackageUploader.cs.meta
│ │ │ │ │ │ ├── UploadStatus.cs
│ │ │ │ │ │ └── UploadStatus.cs.meta
│ │ │ │ │ ├── Api.meta
│ │ │ │ │ ├── AssemblyInfo.cs
│ │ │ │ │ ├── AssemblyInfo.cs.meta
│ │ │ │ │ ├── AssetStoreTools.cs
│ │ │ │ │ ├── AssetStoreTools.cs.meta
│ │ │ │ │ ├── AssetStoreToolsWindow.cs
│ │ │ │ │ ├── AssetStoreToolsWindow.cs.meta
│ │ │ │ │ ├── Constants.cs
│ │ │ │ │ ├── Constants.cs.meta
│ │ │ │ │ ├── Exporter/
│ │ │ │ │ │ ├── Abstractions/
│ │ │ │ │ │ │ ├── IPackageExporter.cs
│ │ │ │ │ │ │ ├── IPackageExporter.cs.meta
│ │ │ │ │ │ │ ├── IPreviewInjector.cs
│ │ │ │ │ │ │ ├── IPreviewInjector.cs.meta
│ │ │ │ │ │ │ ├── PackageExporterBase.cs
│ │ │ │ │ │ │ ├── PackageExporterBase.cs.meta
│ │ │ │ │ │ │ ├── PackageExporterSettings.cs
│ │ │ │ │ │ │ └── PackageExporterSettings.cs.meta
│ │ │ │ │ │ ├── Abstractions.meta
│ │ │ │ │ │ ├── DefaultExporterSettings.cs
│ │ │ │ │ │ ├── DefaultExporterSettings.cs.meta
│ │ │ │ │ │ ├── DefaultPackageExporter.cs
│ │ │ │ │ │ ├── DefaultPackageExporter.cs.meta
│ │ │ │ │ │ ├── LegacyExporterSettings.cs
│ │ │ │ │ │ ├── LegacyExporterSettings.cs.meta
│ │ │ │ │ │ ├── LegacyPackageExporter.cs
│ │ │ │ │ │ ├── LegacyPackageExporter.cs.meta
│ │ │ │ │ │ ├── PackageExporterResult.cs
│ │ │ │ │ │ ├── PackageExporterResult.cs.meta
│ │ │ │ │ │ ├── PreviewInjector.cs
│ │ │ │ │ │ └── PreviewInjector.cs.meta
│ │ │ │ │ ├── Exporter.meta
│ │ │ │ │ ├── Previews/
│ │ │ │ │ │ ├── Scripts/
│ │ │ │ │ │ │ ├── Data/
│ │ │ │ │ │ │ │ ├── CustomPreviewGenerationSettings.cs
│ │ │ │ │ │ │ │ ├── CustomPreviewGenerationSettings.cs.meta
│ │ │ │ │ │ │ │ ├── FileNameFormat.cs
│ │ │ │ │ │ │ │ ├── FileNameFormat.cs.meta
│ │ │ │ │ │ │ │ ├── GenerationType.cs
│ │ │ │ │ │ │ │ ├── GenerationType.cs.meta
│ │ │ │ │ │ │ │ ├── NativePreviewGenerationSettings.cs
│ │ │ │ │ │ │ │ ├── NativePreviewGenerationSettings.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewDatabase.cs
│ │ │ │ │ │ │ │ ├── PreviewDatabase.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewFormat.cs
│ │ │ │ │ │ │ │ ├── PreviewFormat.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewGenerationResult.cs
│ │ │ │ │ │ │ │ ├── PreviewGenerationResult.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewGenerationSettings.cs
│ │ │ │ │ │ │ │ ├── PreviewGenerationSettings.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewMetadata.cs
│ │ │ │ │ │ │ │ └── PreviewMetadata.cs.meta
│ │ │ │ │ │ │ ├── Data.meta
│ │ │ │ │ │ │ ├── Generators/
│ │ │ │ │ │ │ │ ├── Custom/
│ │ │ │ │ │ │ │ │ ├── AudioChannel.cs
│ │ │ │ │ │ │ │ │ ├── AudioChannel.cs.meta
│ │ │ │ │ │ │ │ │ ├── AudioChannelCoordinate.cs
│ │ │ │ │ │ │ │ │ ├── AudioChannelCoordinate.cs.meta
│ │ │ │ │ │ │ │ │ ├── Screenshotters/
│ │ │ │ │ │ │ │ │ │ ├── ISceneScreenshotter.cs
│ │ │ │ │ │ │ │ │ │ ├── ISceneScreenshotter.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── MaterialScreenshotter.cs
│ │ │ │ │ │ │ │ │ │ ├── MaterialScreenshotter.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── MeshScreenshotter.cs
│ │ │ │ │ │ │ │ │ │ ├── MeshScreenshotter.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── SceneScreenshotterBase.cs
│ │ │ │ │ │ │ │ │ │ ├── SceneScreenshotterBase.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── SceneScreenshotterSettings.cs
│ │ │ │ │ │ │ │ │ │ └── SceneScreenshotterSettings.cs.meta
│ │ │ │ │ │ │ │ │ ├── Screenshotters.meta
│ │ │ │ │ │ │ │ │ ├── TypeGenerators/
│ │ │ │ │ │ │ │ │ │ ├── AudioTypeGeneratorSettings.cs
│ │ │ │ │ │ │ │ │ │ ├── AudioTypeGeneratorSettings.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── AudioTypePreviewGenerator.cs
│ │ │ │ │ │ │ │ │ │ ├── AudioTypePreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── ITypePreviewGenerator.cs
│ │ │ │ │ │ │ │ │ │ ├── ITypePreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── MaterialTypePreviewGenerator.cs
│ │ │ │ │ │ │ │ │ │ ├── MaterialTypePreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── ModelTypePreviewGenerator.cs
│ │ │ │ │ │ │ │ │ │ ├── ModelTypePreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── PrefabTypePreviewGenerator.cs
│ │ │ │ │ │ │ │ │ │ ├── PrefabTypePreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── TextureTypeGeneratorSettings.cs
│ │ │ │ │ │ │ │ │ │ ├── TextureTypeGeneratorSettings.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── TextureTypePreviewGenerator.cs
│ │ │ │ │ │ │ │ │ │ ├── TextureTypePreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── TypeGeneratorSettings.cs
│ │ │ │ │ │ │ │ │ │ ├── TypeGeneratorSettings.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── TypePreviewGeneratorBase.cs
│ │ │ │ │ │ │ │ │ │ ├── TypePreviewGeneratorBase.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── TypePreviewGeneratorFromScene.cs
│ │ │ │ │ │ │ │ │ │ ├── TypePreviewGeneratorFromScene.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── TypePreviewGeneratorFromSceneSettings.cs
│ │ │ │ │ │ │ │ │ │ └── TypePreviewGeneratorFromSceneSettings.cs.meta
│ │ │ │ │ │ │ │ │ └── TypeGenerators.meta
│ │ │ │ │ │ │ │ ├── Custom.meta
│ │ │ │ │ │ │ │ ├── CustomPreviewGenerator.cs
│ │ │ │ │ │ │ │ ├── CustomPreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ ├── IPreviewGenerator.cs
│ │ │ │ │ │ │ │ ├── IPreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ ├── NativePreviewGenerator.cs
│ │ │ │ │ │ │ │ ├── NativePreviewGenerator.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewGeneratorBase.cs
│ │ │ │ │ │ │ │ └── PreviewGeneratorBase.cs.meta
│ │ │ │ │ │ │ ├── Generators.meta
│ │ │ │ │ │ │ ├── Services/
│ │ │ │ │ │ │ │ ├── Caching/
│ │ │ │ │ │ │ │ │ ├── CachingService.cs
│ │ │ │ │ │ │ │ │ ├── CachingService.cs.meta
│ │ │ │ │ │ │ │ │ ├── ICachingService.cs
│ │ │ │ │ │ │ │ │ └── ICachingService.cs.meta
│ │ │ │ │ │ │ │ ├── Caching.meta
│ │ │ │ │ │ │ │ ├── IPreviewService.cs
│ │ │ │ │ │ │ │ ├── IPreviewService.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewServiceProvider.cs
│ │ │ │ │ │ │ │ └── PreviewServiceProvider.cs.meta
│ │ │ │ │ │ │ ├── Services.meta
│ │ │ │ │ │ │ ├── UI/
│ │ │ │ │ │ │ │ ├── Data/
│ │ │ │ │ │ │ │ │ ├── AssetPreview.cs
│ │ │ │ │ │ │ │ │ ├── AssetPreview.cs.meta
│ │ │ │ │ │ │ │ │ ├── AssetPreviewCollection.cs
│ │ │ │ │ │ │ │ │ ├── AssetPreviewCollection.cs.meta
│ │ │ │ │ │ │ │ │ ├── IAssetPreview.cs
│ │ │ │ │ │ │ │ │ ├── IAssetPreview.cs.meta
│ │ │ │ │ │ │ │ │ ├── IAssetPreviewCollection.cs
│ │ │ │ │ │ │ │ │ ├── IAssetPreviewCollection.cs.meta
│ │ │ │ │ │ │ │ │ ├── IPreviewGeneratorSettings.cs
│ │ │ │ │ │ │ │ │ ├── IPreviewGeneratorSettings.cs.meta
│ │ │ │ │ │ │ │ │ ├── PreviewGeneratorSettings.cs
│ │ │ │ │ │ │ │ │ └── PreviewGeneratorSettings.cs.meta
│ │ │ │ │ │ │ │ ├── Data.meta
│ │ │ │ │ │ │ │ ├── Elements/
│ │ │ │ │ │ │ │ │ ├── AssetPreviewElement.cs
│ │ │ │ │ │ │ │ │ ├── AssetPreviewElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── GridListElement.cs
│ │ │ │ │ │ │ │ │ ├── GridListElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PreviewCollectionElement.cs
│ │ │ │ │ │ │ │ │ ├── PreviewCollectionElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PreviewGenerateButtonElement.cs
│ │ │ │ │ │ │ │ │ ├── PreviewGenerateButtonElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PreviewGeneratorPathsElement.cs
│ │ │ │ │ │ │ │ │ ├── PreviewGeneratorPathsElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PreviewGeneratorSettingsElement.cs
│ │ │ │ │ │ │ │ │ ├── PreviewGeneratorSettingsElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PreviewWindowDescriptionElement.cs
│ │ │ │ │ │ │ │ │ └── PreviewWindowDescriptionElement.cs.meta
│ │ │ │ │ │ │ │ ├── Elements.meta
│ │ │ │ │ │ │ │ ├── PreviewGeneratorWindow.cs
│ │ │ │ │ │ │ │ ├── PreviewGeneratorWindow.cs.meta
│ │ │ │ │ │ │ │ ├── Views/
│ │ │ │ │ │ │ │ │ ├── PreviewListView.cs
│ │ │ │ │ │ │ │ │ └── PreviewListView.cs.meta
│ │ │ │ │ │ │ │ └── Views.meta
│ │ │ │ │ │ │ ├── UI.meta
│ │ │ │ │ │ │ ├── Utility/
│ │ │ │ │ │ │ │ ├── GraphicsUtility.cs
│ │ │ │ │ │ │ │ ├── GraphicsUtility.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewConvertUtility.cs
│ │ │ │ │ │ │ │ ├── PreviewConvertUtility.cs.meta
│ │ │ │ │ │ │ │ ├── PreviewSceneUtility.cs
│ │ │ │ │ │ │ │ ├── PreviewSceneUtility.cs.meta
│ │ │ │ │ │ │ │ ├── RenderPipeline.cs
│ │ │ │ │ │ │ │ ├── RenderPipeline.cs.meta
│ │ │ │ │ │ │ │ ├── RenderPipelineUtility.cs
│ │ │ │ │ │ │ │ └── RenderPipelineUtility.cs.meta
│ │ │ │ │ │ │ └── Utility.meta
│ │ │ │ │ │ ├── Scripts.meta
│ │ │ │ │ │ ├── Styles/
│ │ │ │ │ │ │ ├── Style.uss
│ │ │ │ │ │ │ ├── Style.uss.meta
│ │ │ │ │ │ │ ├── ThemeDark.uss
│ │ │ │ │ │ │ ├── ThemeDark.uss.meta
│ │ │ │ │ │ │ ├── ThemeLight.uss
│ │ │ │ │ │ │ └── ThemeLight.uss.meta
│ │ │ │ │ │ └── Styles.meta
│ │ │ │ │ ├── Previews.meta
│ │ │ │ │ ├── Unity.AssetStoreTools.Editor.asmdef
│ │ │ │ │ ├── Unity.AssetStoreTools.Editor.asmdef.meta
│ │ │ │ │ ├── Uploader/
│ │ │ │ │ │ ├── Icons/
│ │ │ │ │ │ │ ├── account-dark.png.meta
│ │ │ │ │ │ │ ├── account-light.png.meta
│ │ │ │ │ │ │ ├── open-in-browser.png.meta
│ │ │ │ │ │ │ ├── publisher-portal-dark.png.meta
│ │ │ │ │ │ │ └── publisher-portal-light.png.meta
│ │ │ │ │ │ ├── Icons.meta
│ │ │ │ │ │ ├── Scripts/
│ │ │ │ │ │ │ ├── Data/
│ │ │ │ │ │ │ │ ├── Abstractions/
│ │ │ │ │ │ │ │ │ ├── IPackage.cs
│ │ │ │ │ │ │ │ │ ├── IPackage.cs.meta
│ │ │ │ │ │ │ │ │ ├── IPackageContent.cs
│ │ │ │ │ │ │ │ │ ├── IPackageContent.cs.meta
│ │ │ │ │ │ │ │ │ ├── IPackageGroup.cs
│ │ │ │ │ │ │ │ │ ├── IPackageGroup.cs.meta
│ │ │ │ │ │ │ │ │ ├── IWorkflow.cs
│ │ │ │ │ │ │ │ │ ├── IWorkflow.cs.meta
│ │ │ │ │ │ │ │ │ ├── IWorkflowServices.cs
│ │ │ │ │ │ │ │ │ ├── IWorkflowServices.cs.meta
│ │ │ │ │ │ │ │ │ ├── WorkflowBase.cs
│ │ │ │ │ │ │ │ │ └── WorkflowBase.cs.meta
│ │ │ │ │ │ │ │ ├── Abstractions.meta
│ │ │ │ │ │ │ │ ├── AssetsWorkflow.cs
│ │ │ │ │ │ │ │ ├── AssetsWorkflow.cs.meta
│ │ │ │ │ │ │ │ ├── HybridPackageWorkflow.cs
│ │ │ │ │ │ │ │ ├── HybridPackageWorkflow.cs.meta
│ │ │ │ │ │ │ │ ├── Package.cs
│ │ │ │ │ │ │ │ ├── Package.cs.meta
│ │ │ │ │ │ │ │ ├── PackageContent.cs
│ │ │ │ │ │ │ │ ├── PackageContent.cs.meta
│ │ │ │ │ │ │ │ ├── PackageGroup.cs
│ │ │ │ │ │ │ │ ├── PackageGroup.cs.meta
│ │ │ │ │ │ │ │ ├── PackageSorting.cs
│ │ │ │ │ │ │ │ ├── PackageSorting.cs.meta
│ │ │ │ │ │ │ │ ├── Serialization/
│ │ │ │ │ │ │ │ │ ├── AssetPath.cs
│ │ │ │ │ │ │ │ │ ├── AssetPath.cs.meta
│ │ │ │ │ │ │ │ │ ├── AssetsWorkflowStateData.cs
│ │ │ │ │ │ │ │ │ ├── AssetsWorkflowStateData.cs.meta
│ │ │ │ │ │ │ │ │ ├── HybridPackageWorkflowState.cs
│ │ │ │ │ │ │ │ │ ├── HybridPackageWorkflowState.cs.meta
│ │ │ │ │ │ │ │ │ ├── UnityPackageWorkflowStateData.cs
│ │ │ │ │ │ │ │ │ ├── UnityPackageWorkflowStateData.cs.meta
│ │ │ │ │ │ │ │ │ ├── WorkflowStateData.cs
│ │ │ │ │ │ │ │ │ └── WorkflowStateData.cs.meta
│ │ │ │ │ │ │ │ ├── Serialization.meta
│ │ │ │ │ │ │ │ ├── UnityPackageWorkflow.cs
│ │ │ │ │ │ │ │ ├── UnityPackageWorkflow.cs.meta
│ │ │ │ │ │ │ │ ├── WorkflowServices.cs
│ │ │ │ │ │ │ │ └── WorkflowServices.cs.meta
│ │ │ │ │ │ │ ├── Data.meta
│ │ │ │ │ │ │ ├── Services/
│ │ │ │ │ │ │ │ ├── Analytics/
│ │ │ │ │ │ │ │ │ ├── AnalyticsService.cs
│ │ │ │ │ │ │ │ │ ├── AnalyticsService.cs.meta
│ │ │ │ │ │ │ │ │ ├── Data/
│ │ │ │ │ │ │ │ │ │ ├── AuthenticationAnalytic.cs
│ │ │ │ │ │ │ │ │ │ ├── AuthenticationAnalytic.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── BaseAnalytic.cs
│ │ │ │ │ │ │ │ │ │ ├── BaseAnalytic.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IAssetStoreAnalytic.cs
│ │ │ │ │ │ │ │ │ │ ├── IAssetStoreAnalytic.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IAssetStoreAnalyticData.cs
│ │ │ │ │ │ │ │ │ │ ├── IAssetStoreAnalyticData.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── PackageUploadAnalytic.cs
│ │ │ │ │ │ │ │ │ │ ├── PackageUploadAnalytic.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── ValidationResultsSerializer.cs
│ │ │ │ │ │ │ │ │ │ └── ValidationResultsSerializer.cs.meta
│ │ │ │ │ │ │ │ │ ├── Data.meta
│ │ │ │ │ │ │ │ │ ├── IAnalyticsService.cs
│ │ │ │ │ │ │ │ │ └── IAnalyticsService.cs.meta
│ │ │ │ │ │ │ │ ├── Analytics.meta
│ │ │ │ │ │ │ │ ├── Api/
│ │ │ │ │ │ │ │ │ ├── AuthenticationService.cs
│ │ │ │ │ │ │ │ │ ├── AuthenticationService.cs.meta
│ │ │ │ │ │ │ │ │ ├── IAuthenticationService.cs
│ │ │ │ │ │ │ │ │ ├── IAuthenticationService.cs.meta
│ │ │ │ │ │ │ │ │ ├── IPackageDownloadingService.cs
│ │ │ │ │ │ │ │ │ ├── IPackageDownloadingService.cs.meta
│ │ │ │ │ │ │ │ │ ├── IPackageUploadingService.cs
│ │ │ │ │ │ │ │ │ ├── IPackageUploadingService.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageDownloadingService.cs
│ │ │ │ │ │ │ │ │ ├── PackageDownloadingService.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageUploadingService.cs
│ │ │ │ │ │ │ │ │ └── PackageUploadingService.cs.meta
│ │ │ │ │ │ │ │ ├── Api.meta
│ │ │ │ │ │ │ │ ├── Caching/
│ │ │ │ │ │ │ │ │ ├── CachingService.cs
│ │ │ │ │ │ │ │ │ ├── CachingService.cs.meta
│ │ │ │ │ │ │ │ │ ├── ICachingService.cs
│ │ │ │ │ │ │ │ │ └── ICachingService.cs.meta
│ │ │ │ │ │ │ │ ├── Caching.meta
│ │ │ │ │ │ │ │ ├── IUploaderService.cs
│ │ │ │ │ │ │ │ ├── IUploaderService.cs.meta
│ │ │ │ │ │ │ │ ├── PackageFactory/
│ │ │ │ │ │ │ │ │ ├── IPackageFactoryService.cs
│ │ │ │ │ │ │ │ │ ├── IPackageFactoryService.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageFactoryService.cs
│ │ │ │ │ │ │ │ │ └── PackageFactoryService.cs.meta
│ │ │ │ │ │ │ │ ├── PackageFactory.meta
│ │ │ │ │ │ │ │ ├── UploaderServiceProvider.cs
│ │ │ │ │ │ │ │ └── UploaderServiceProvider.cs.meta
│ │ │ │ │ │ │ ├── Services.meta
│ │ │ │ │ │ │ ├── UI/
│ │ │ │ │ │ │ │ ├── Elements/
│ │ │ │ │ │ │ │ │ ├── Abstractions/
│ │ │ │ │ │ │ │ │ │ ├── ValidationElementBase.cs
│ │ │ │ │ │ │ │ │ │ ├── ValidationElementBase.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── WorkflowElementBase.cs
│ │ │ │ │ │ │ │ │ │ └── WorkflowElementBase.cs.meta
│ │ │ │ │ │ │ │ │ ├── Abstractions.meta
│ │ │ │ │ │ │ │ │ ├── AccountToolbar.cs
│ │ │ │ │ │ │ │ │ ├── AccountToolbar.cs.meta
│ │ │ │ │ │ │ │ │ ├── AssetsWorkflowElement.cs
│ │ │ │ │ │ │ │ │ ├── AssetsWorkflowElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── CurrentProjectValidationElement.cs
│ │ │ │ │ │ │ │ │ ├── CurrentProjectValidationElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── ExternalProjectValidationElement.cs
│ │ │ │ │ │ │ │ │ ├── ExternalProjectValidationElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── HybridPackageWorkflowElement.cs
│ │ │ │ │ │ │ │ │ ├── HybridPackageWorkflowElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── LoadingSpinner.cs
│ │ │ │ │ │ │ │ │ ├── LoadingSpinner.cs.meta
│ │ │ │ │ │ │ │ │ ├── MultiToggleSelectionElement.cs
│ │ │ │ │ │ │ │ │ ├── MultiToggleSelectionElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageContentElement.cs
│ │ │ │ │ │ │ │ │ ├── PackageContentElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageElement.cs
│ │ │ │ │ │ │ │ │ ├── PackageElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageGroupElement.cs
│ │ │ │ │ │ │ │ │ ├── PackageGroupElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageListToolbar.cs
│ │ │ │ │ │ │ │ │ ├── PackageListToolbar.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageUploadElement.cs
│ │ │ │ │ │ │ │ │ ├── PackageUploadElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PathSelectionElement.cs
│ │ │ │ │ │ │ │ │ ├── PathSelectionElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── PreviewGenerationElement.cs
│ │ │ │ │ │ │ │ │ ├── PreviewGenerationElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── UnityPackageWorkflowElement.cs
│ │ │ │ │ │ │ │ │ └── UnityPackageWorkflowElement.cs.meta
│ │ │ │ │ │ │ │ ├── Elements.meta
│ │ │ │ │ │ │ │ ├── Views/
│ │ │ │ │ │ │ │ │ ├── LoginView.cs
│ │ │ │ │ │ │ │ │ ├── LoginView.cs.meta
│ │ │ │ │ │ │ │ │ ├── PackageListView.cs
│ │ │ │ │ │ │ │ │ └── PackageListView.cs.meta
│ │ │ │ │ │ │ │ └── Views.meta
│ │ │ │ │ │ │ └── UI.meta
│ │ │ │ │ │ ├── Scripts.meta
│ │ │ │ │ │ ├── Styles/
│ │ │ │ │ │ │ ├── LoginView/
│ │ │ │ │ │ │ │ ├── Style.uss
│ │ │ │ │ │ │ │ ├── Style.uss.meta
│ │ │ │ │ │ │ │ ├── ThemeDark.uss
│ │ │ │ │ │ │ │ ├── ThemeDark.uss.meta
│ │ │ │ │ │ │ │ ├── ThemeLight.uss
│ │ │ │ │ │ │ │ └── ThemeLight.uss.meta
│ │ │ │ │ │ │ ├── LoginView.meta
│ │ │ │ │ │ │ ├── PackageListView/
│ │ │ │ │ │ │ │ ├── Style.uss
│ │ │ │ │ │ │ │ ├── Style.uss.meta
│ │ │ │ │ │ │ │ ├── ThemeDark.uss
│ │ │ │ │ │ │ │ ├── ThemeDark.uss.meta
│ │ │ │ │ │ │ │ ├── ThemeLight.uss
│ │ │ │ │ │ │ │ └── ThemeLight.uss.meta
│ │ │ │ │ │ │ ├── PackageListView.meta
│ │ │ │ │ │ │ ├── Style.uss
│ │ │ │ │ │ │ ├── Style.uss.meta
│ │ │ │ │ │ │ ├── ThemeDark.uss
│ │ │ │ │ │ │ ├── ThemeDark.uss.meta
│ │ │ │ │ │ │ ├── ThemeLight.uss
│ │ │ │ │ │ │ └── ThemeLight.uss.meta
│ │ │ │ │ │ ├── Styles.meta
│ │ │ │ │ │ ├── UploaderWindow.cs
│ │ │ │ │ │ └── UploaderWindow.cs.meta
│ │ │ │ │ ├── Uploader.meta
│ │ │ │ │ ├── Utility/
│ │ │ │ │ │ ├── ASDebug.cs
│ │ │ │ │ │ ├── ASDebug.cs.meta
│ │ │ │ │ │ ├── ASToolsPreferences.cs
│ │ │ │ │ │ ├── ASToolsPreferences.cs.meta
│ │ │ │ │ │ ├── ASToolsUpdater.cs
│ │ │ │ │ │ ├── ASToolsUpdater.cs.meta
│ │ │ │ │ │ ├── CacheUtil.cs
│ │ │ │ │ │ ├── CacheUtil.cs.meta
│ │ │ │ │ │ ├── FileUtility.cs
│ │ │ │ │ │ ├── FileUtility.cs.meta
│ │ │ │ │ │ ├── LegacyToolsRemover.cs
│ │ │ │ │ │ ├── LegacyToolsRemover.cs.meta
│ │ │ │ │ │ ├── PackageUtility.cs
│ │ │ │ │ │ ├── PackageUtility.cs.meta
│ │ │ │ │ │ ├── ServiceProvider.cs
│ │ │ │ │ │ ├── ServiceProvider.cs.meta
│ │ │ │ │ │ ├── StyleSelector.cs
│ │ │ │ │ │ ├── StyleSelector.cs.meta
│ │ │ │ │ │ ├── Styles/
│ │ │ │ │ │ │ ├── Updater/
│ │ │ │ │ │ │ │ ├── Style.uss
│ │ │ │ │ │ │ │ ├── Style.uss.meta
│ │ │ │ │ │ │ │ ├── ThemeDark.uss
│ │ │ │ │ │ │ │ ├── ThemeDark.uss.meta
│ │ │ │ │ │ │ │ ├── ThemeLight.uss
│ │ │ │ │ │ │ │ └── ThemeLight.uss.meta
│ │ │ │ │ │ │ └── Updater.meta
│ │ │ │ │ │ ├── Styles.meta
│ │ │ │ │ │ ├── SymlinkUtil.cs
│ │ │ │ │ │ └── SymlinkUtil.cs.meta
│ │ │ │ │ ├── Utility.meta
│ │ │ │ │ ├── Validator/
│ │ │ │ │ │ ├── Icons/
│ │ │ │ │ │ │ ├── error.png.meta
│ │ │ │ │ │ │ ├── error_d.png.meta
│ │ │ │ │ │ │ ├── success.png.meta
│ │ │ │ │ │ │ ├── success_d.png.meta
│ │ │ │ │ │ │ ├── undefined.png.meta
│ │ │ │ │ │ │ ├── undefined_d.png.meta
│ │ │ │ │ │ │ ├── warning.png.meta
│ │ │ │ │ │ │ └── warning_d.png.meta
│ │ │ │ │ │ ├── Icons.meta
│ │ │ │ │ │ ├── Scripts/
│ │ │ │ │ │ │ ├── Categories/
│ │ │ │ │ │ │ │ ├── CategoryEvaluator.cs
│ │ │ │ │ │ │ │ ├── CategoryEvaluator.cs.meta
│ │ │ │ │ │ │ │ ├── ValidatorCategory.cs
│ │ │ │ │ │ │ │ └── ValidatorCategory.cs.meta
│ │ │ │ │ │ │ ├── Categories.meta
│ │ │ │ │ │ │ ├── CurrentProjectValidator.cs
│ │ │ │ │ │ │ ├── CurrentProjectValidator.cs.meta
│ │ │ │ │ │ │ ├── Data/
│ │ │ │ │ │ │ │ ├── CurrentProjectValidationSettings.cs
│ │ │ │ │ │ │ │ ├── CurrentProjectValidationSettings.cs.meta
│ │ │ │ │ │ │ │ ├── ExternalProjectValidationSettings.cs
│ │ │ │ │ │ │ │ ├── ExternalProjectValidationSettings.cs.meta
│ │ │ │ │ │ │ │ ├── MessageActions/
│ │ │ │ │ │ │ │ │ ├── HighlightObjectAction.cs
│ │ │ │ │ │ │ │ │ ├── HighlightObjectAction.cs.meta
│ │ │ │ │ │ │ │ │ ├── IMessageAction.cs
│ │ │ │ │ │ │ │ │ ├── IMessageAction.cs.meta
│ │ │ │ │ │ │ │ │ ├── OpenAssetAction.cs
│ │ │ │ │ │ │ │ │ └── OpenAssetAction.cs.meta
│ │ │ │ │ │ │ │ ├── MessageActions.meta
│ │ │ │ │ │ │ │ ├── TestResult.cs
│ │ │ │ │ │ │ │ ├── TestResult.cs.meta
│ │ │ │ │ │ │ │ ├── TestResultMessage.cs
│ │ │ │ │ │ │ │ ├── TestResultMessage.cs.meta
│ │ │ │ │ │ │ │ ├── TestResultObject.cs
│ │ │ │ │ │ │ │ ├── TestResultObject.cs.meta
│ │ │ │ │ │ │ │ ├── TestResultStatus.cs
│ │ │ │ │ │ │ │ ├── TestResultStatus.cs.meta
│ │ │ │ │ │ │ │ ├── ValidationResult.cs
│ │ │ │ │ │ │ │ ├── ValidationResult.cs.meta
│ │ │ │ │ │ │ │ ├── ValidationSettings.cs
│ │ │ │ │ │ │ │ ├── ValidationSettings.cs.meta
│ │ │ │ │ │ │ │ ├── ValidationStatus.cs
│ │ │ │ │ │ │ │ ├── ValidationStatus.cs.meta
│ │ │ │ │ │ │ │ ├── ValidationType.cs
│ │ │ │ │ │ │ │ └── ValidationType.cs.meta
│ │ │ │ │ │ │ ├── Data.meta
│ │ │ │ │ │ │ ├── ExternalProjectValidator.cs
│ │ │ │ │ │ │ ├── ExternalProjectValidator.cs.meta
│ │ │ │ │ │ │ ├── IValidator.cs
│ │ │ │ │ │ │ ├── IValidator.cs.meta
│ │ │ │ │ │ │ ├── Services/
│ │ │ │ │ │ │ │ ├── CachingService/
│ │ │ │ │ │ │ │ │ ├── CachingService.cs
│ │ │ │ │ │ │ │ │ ├── CachingService.cs.meta
│ │ │ │ │ │ │ │ │ ├── ICachingService.cs
│ │ │ │ │ │ │ │ │ ├── ICachingService.cs.meta
│ │ │ │ │ │ │ │ │ ├── PreviewDatabaseContractResolver.cs
│ │ │ │ │ │ │ │ │ └── PreviewDatabaseContractResolver.cs.meta
│ │ │ │ │ │ │ │ ├── CachingService.meta
│ │ │ │ │ │ │ │ ├── IValidatorService.cs
│ │ │ │ │ │ │ │ ├── IValidatorService.cs.meta
│ │ │ │ │ │ │ │ ├── Validation/
│ │ │ │ │ │ │ │ │ ├── Abstractions/
│ │ │ │ │ │ │ │ │ │ ├── IAssetUtilityService.cs
│ │ │ │ │ │ │ │ │ │ ├── IAssetUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IFileSignatureUtilityService.cs
│ │ │ │ │ │ │ │ │ │ ├── IFileSignatureUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IMeshUtilityService.cs
│ │ │ │ │ │ │ │ │ │ ├── IMeshUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IModelUtilityService.cs
│ │ │ │ │ │ │ │ │ │ ├── IModelUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── ISceneUtilityService.cs
│ │ │ │ │ │ │ │ │ │ ├── ISceneUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IScriptUtilityService.cs
│ │ │ │ │ │ │ │ │ │ └── IScriptUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ ├── Abstractions.meta
│ │ │ │ │ │ │ │ │ ├── AssetUtilityService.cs
│ │ │ │ │ │ │ │ │ ├── AssetUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ ├── Data/
│ │ │ │ │ │ │ │ │ │ ├── ArchiveType.cs
│ │ │ │ │ │ │ │ │ │ ├── ArchiveType.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── AssetEnumerator.cs
│ │ │ │ │ │ │ │ │ │ ├── AssetEnumerator.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── AssetType.cs
│ │ │ │ │ │ │ │ │ │ ├── AssetType.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── LogEntry.cs
│ │ │ │ │ │ │ │ │ │ └── LogEntry.cs.meta
│ │ │ │ │ │ │ │ │ ├── Data.meta
│ │ │ │ │ │ │ │ │ ├── FileSignatureUtilityService.cs
│ │ │ │ │ │ │ │ │ ├── FileSignatureUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ ├── MeshUtilityService.cs
│ │ │ │ │ │ │ │ │ ├── MeshUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ ├── ModelUtilityService.cs
│ │ │ │ │ │ │ │ │ ├── ModelUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ ├── SceneUtilityService.cs
│ │ │ │ │ │ │ │ │ ├── SceneUtilityService.cs.meta
│ │ │ │ │ │ │ │ │ ├── ScriptUtilityService.cs
│ │ │ │ │ │ │ │ │ └── ScriptUtilityService.cs.meta
│ │ │ │ │ │ │ │ ├── Validation.meta
│ │ │ │ │ │ │ │ ├── ValidatorServiceProvider.cs
│ │ │ │ │ │ │ │ └── ValidatorServiceProvider.cs.meta
│ │ │ │ │ │ │ ├── Services.meta
│ │ │ │ │ │ │ ├── Test Definitions/
│ │ │ │ │ │ │ │ ├── AutomatedTest.cs
│ │ │ │ │ │ │ │ ├── AutomatedTest.cs.meta
│ │ │ │ │ │ │ │ ├── GenericTestConfig.cs
│ │ │ │ │ │ │ │ ├── GenericTestConfig.cs.meta
│ │ │ │ │ │ │ │ ├── ITestConfig.cs
│ │ │ │ │ │ │ │ ├── ITestConfig.cs.meta
│ │ │ │ │ │ │ │ ├── ITestScript.cs
│ │ │ │ │ │ │ │ ├── ITestScript.cs.meta
│ │ │ │ │ │ │ │ ├── Scriptable Objects/
│ │ │ │ │ │ │ │ │ ├── AutomatedTestScriptableObject.cs
│ │ │ │ │ │ │ │ │ ├── AutomatedTestScriptableObject.cs.meta
│ │ │ │ │ │ │ │ │ ├── Editor/
│ │ │ │ │ │ │ │ │ │ ├── ValidationTestScriptableObjectInspector.cs
│ │ │ │ │ │ │ │ │ │ └── ValidationTestScriptableObjectInspector.cs.meta
│ │ │ │ │ │ │ │ │ ├── Editor.meta
│ │ │ │ │ │ │ │ │ ├── ValidationTestScriptableObject.cs
│ │ │ │ │ │ │ │ │ └── ValidationTestScriptableObject.cs.meta
│ │ │ │ │ │ │ │ ├── Scriptable Objects.meta
│ │ │ │ │ │ │ │ ├── ValidationTest.cs
│ │ │ │ │ │ │ │ └── ValidationTest.cs.meta
│ │ │ │ │ │ │ ├── Test Definitions.meta
│ │ │ │ │ │ │ ├── Test Methods/
│ │ │ │ │ │ │ │ ├── Generic/
│ │ │ │ │ │ │ │ │ ├── CheckAnimationClips.cs
│ │ │ │ │ │ │ │ │ ├── CheckAnimationClips.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckAudioClipping.cs
│ │ │ │ │ │ │ │ │ ├── CheckAudioClipping.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckColliders.cs
│ │ │ │ │ │ │ │ │ ├── CheckColliders.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckCompressedFiles.cs
│ │ │ │ │ │ │ │ │ ├── CheckCompressedFiles.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckEmptyPrefabs.cs
│ │ │ │ │ │ │ │ │ ├── CheckEmptyPrefabs.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckFileMenuNames.cs
│ │ │ │ │ │ │ │ │ ├── CheckFileMenuNames.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckLODs.cs
│ │ │ │ │ │ │ │ │ ├── CheckLODs.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckLineEndings.cs
│ │ │ │ │ │ │ │ │ ├── CheckLineEndings.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckMeshPrefabs.cs
│ │ │ │ │ │ │ │ │ ├── CheckMeshPrefabs.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckMissingComponentsinAssets.cs
│ │ │ │ │ │ │ │ │ ├── CheckMissingComponentsinAssets.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckMissingComponentsinScenes.cs
│ │ │ │ │ │ │ │ │ ├── CheckMissingComponentsinScenes.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckModelImportLogs.cs
│ │ │ │ │ │ │ │ │ ├── CheckModelImportLogs.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckModelOrientation.cs
│ │ │ │ │ │ │ │ │ ├── CheckModelOrientation.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckModelTypes.cs
│ │ │ │ │ │ │ │ │ ├── CheckModelTypes.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckNormalMapTextures.cs
│ │ │ │ │ │ │ │ │ ├── CheckNormalMapTextures.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckPackageNaming.cs
│ │ │ │ │ │ │ │ │ ├── CheckPackageNaming.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckParticleSystems.cs
│ │ │ │ │ │ │ │ │ ├── CheckParticleSystems.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckPathLengths.cs
│ │ │ │ │ │ │ │ │ ├── CheckPathLengths.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckPrefabTransforms.cs
│ │ │ │ │ │ │ │ │ ├── CheckPrefabTransforms.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckScriptCompilation.cs
│ │ │ │ │ │ │ │ │ ├── CheckScriptCompilation.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckShaderCompilation.cs
│ │ │ │ │ │ │ │ │ ├── CheckShaderCompilation.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckTextureDimensions.cs
│ │ │ │ │ │ │ │ │ ├── CheckTextureDimensions.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckTypeNamespaces.cs
│ │ │ │ │ │ │ │ │ ├── CheckTypeNamespaces.cs.meta
│ │ │ │ │ │ │ │ │ ├── RemoveExecutableFiles.cs
│ │ │ │ │ │ │ │ │ ├── RemoveExecutableFiles.cs.meta
│ │ │ │ │ │ │ │ │ ├── RemoveJPGFiles.cs
│ │ │ │ │ │ │ │ │ ├── RemoveJPGFiles.cs.meta
│ │ │ │ │ │ │ │ │ ├── RemoveJavaScriptFiles.cs
│ │ │ │ │ │ │ │ │ ├── RemoveJavaScriptFiles.cs.meta
│ │ │ │ │ │ │ │ │ ├── RemoveLossyAudioFiles.cs
│ │ │ │ │ │ │ │ │ ├── RemoveLossyAudioFiles.cs.meta
│ │ │ │ │ │ │ │ │ ├── RemoveMixamoFiles.cs
│ │ │ │ │ │ │ │ │ ├── RemoveMixamoFiles.cs.meta
│ │ │ │ │ │ │ │ │ ├── RemoveSpeedTreeFiles.cs
│ │ │ │ │ │ │ │ │ ├── RemoveSpeedTreeFiles.cs.meta
│ │ │ │ │ │ │ │ │ ├── RemoveVideoFiles.cs
│ │ │ │ │ │ │ │ │ └── RemoveVideoFiles.cs.meta
│ │ │ │ │ │ │ │ ├── Generic.meta
│ │ │ │ │ │ │ │ ├── UnityPackage/
│ │ │ │ │ │ │ │ │ ├── CheckDemoScenes.cs
│ │ │ │ │ │ │ │ │ ├── CheckDemoScenes.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckDocumentation.cs
│ │ │ │ │ │ │ │ │ ├── CheckDocumentation.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckPackageSize.cs
│ │ │ │ │ │ │ │ │ ├── CheckPackageSize.cs.meta
│ │ │ │ │ │ │ │ │ ├── CheckProjectTemplateAssets.cs
│ │ │ │ │ │ │ │ │ └── CheckProjectTemplateAssets.cs.meta
│ │ │ │ │ │ │ │ └── UnityPackage.meta
│ │ │ │ │ │ │ ├── Test Methods.meta
│ │ │ │ │ │ │ ├── UI/
│ │ │ │ │ │ │ │ ├── Data/
│ │ │ │ │ │ │ │ │ ├── Abstractions/
│ │ │ │ │ │ │ │ │ │ ├── IValidatorResults.cs
│ │ │ │ │ │ │ │ │ │ ├── IValidatorResults.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IValidatorSettings.cs
│ │ │ │ │ │ │ │ │ │ ├── IValidatorSettings.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IValidatorTest.cs
│ │ │ │ │ │ │ │ │ │ ├── IValidatorTest.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── IValidatorTestGroup.cs
│ │ │ │ │ │ │ │ │ │ └── IValidatorTestGroup.cs.meta
│ │ │ │ │ │ │ │ │ ├── Abstractions.meta
│ │ │ │ │ │ │ │ │ ├── Serialization/
│ │ │ │ │ │ │ │ │ │ ├── ValidatorStateData.cs
│ │ │ │ │ │ │ │ │ │ ├── ValidatorStateData.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── ValidatorStateDataContractResolver.cs
│ │ │ │ │ │ │ │ │ │ ├── ValidatorStateDataContractResolver.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── ValidatorStateResults.cs
│ │ │ │ │ │ │ │ │ │ ├── ValidatorStateResults.cs.meta
│ │ │ │ │ │ │ │ │ │ ├── ValidatorStateSettings.cs
│ │ │ │ │ │ │ │ │ │ └── ValidatorStateSettings.cs.meta
│ │ │ │ │ │ │ │ │ ├── Serialization.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorResults.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorResults.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorSettings.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorSettings.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorTest.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorTest.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorTestGroup.cs
│ │ │ │ │ │ │ │ │ └── ValidatorTestGroup.cs.meta
│ │ │ │ │ │ │ │ ├── Data.meta
│ │ │ │ │ │ │ │ ├── Elements/
│ │ │ │ │ │ │ │ │ ├── ValidatorButtonElement.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorButtonElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorDescriptionElement.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorDescriptionElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorPathsElement.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorPathsElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorResultsElement.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorResultsElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorSettingsElement.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorSettingsElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorTestElement.cs
│ │ │ │ │ │ │ │ │ ├── ValidatorTestElement.cs.meta
│ │ │ │ │ │ │ │ │ ├── ValidatorTestGroupElement.cs
│ │ │ │ │ │ │ │ │ └── ValidatorTestGroupElement.cs.meta
│ │ │ │ │ │ │ │ ├── Elements.meta
│ │ │ │ │ │ │ │ ├── ValidatorWindow.cs
│ │ │ │ │ │ │ │ ├── ValidatorWindow.cs.meta
│ │ │ │ │ │ │ │ ├── Views/
│ │ │ │ │ │ │ │ │ ├── ValidatorTestsView.cs
│ │ │ │ │ │ │ │ │ └── ValidatorTestsView.cs.meta
│ │ │ │ │ │ │ │ └── Views.meta
│ │ │ │ │ │ │ ├── UI.meta
│ │ │ │ │ │ │ ├── Utility/
│ │ │ │ │ │ │ │ ├── ValidatorUtility.cs
│ │ │ │ │ │ │ │ └── ValidatorUtility.cs.meta
│ │ │ │ │ │ │ ├── Utility.meta
│ │ │ │ │ │ │ ├── ValidatorBase.cs
│ │ │ │ │ │ │ └── ValidatorBase.cs.meta
│ │ │ │ │ │ ├── Scripts.meta
│ │ │ │ │ │ ├── Styles/
│ │ │ │ │ │ │ ├── Style.uss
│ │ │ │ │ │ │ ├── Style.uss.meta
│ │ │ │ │ │ │ ├── ThemeDark.uss
│ │ │ │ │ │ │ ├── ThemeDark.uss.meta
│ │ │ │ │ │ │ ├── ThemeLight.uss
│ │ │ │ │ │ │ └── ThemeLight.uss.meta
│ │ │ │ │ │ ├── Styles.meta
│ │ │ │ │ │ ├── Tests/
│ │ │ │ │ │ │ ├── Generic/
│ │ │ │ │ │ │ │ ├── Check Animation Clips.asset.meta
│ │ │ │ │ │ │ │ ├── Check Audio Clipping.asset.meta
│ │ │ │ │ │ │ │ ├── Check Colliders.asset.meta
│ │ │ │ │ │ │ │ ├── Check Compressed Files.asset.meta
│ │ │ │ │ │ │ │ ├── Check Empty Prefabs.asset.meta
│ │ │ │ │ │ │ │ ├── Check File Menu Names.asset.meta
│ │ │ │ │ │ │ │ ├── Check LODs.asset.meta
│ │ │ │ │ │ │ │ ├── Check Line Endings.asset.meta
│ │ │ │ │ │ │ │ ├── Check Mesh Prefabs.asset.meta
│ │ │ │ │ │ │ │ ├── Check Missing Components in Assets.asset.meta
│ │ │ │ │ │ │ │ ├── Check Missing Components in Scenes.asset.meta
│ │ │ │ │ │ │ │ ├── Check Model Import Logs.asset.meta
│ │ │ │ │ │ │ │ ├── Check Model Orientation.asset.meta
│ │ │ │ │ │ │ │ ├── Check Model Types.asset.meta
│ │ │ │ │ │ │ │ ├── Check Normal Map Textures.asset.meta
│ │ │ │ │ │ │ │ ├── Check Package Naming.asset.meta
│ │ │ │ │ │ │ │ ├── Check Particle Systems.asset.meta
│ │ │ │ │ │ │ │ ├── Check Path Lengths.asset.meta
│ │ │ │ │ │ │ │ ├── Check Prefab Transforms.asset.meta
│ │ │ │ │ │ │ │ ├── Check Script Compilation.asset.meta
│ │ │ │ │ │ │ │ ├── Check Shader Compilation.asset.meta
│ │ │ │ │ │ │ │ ├── Check Texture Dimensions.asset.meta
│ │ │ │ │ │ │ │ ├── Check Type Namespaces.asset.meta
│ │ │ │ │ │ │ │ ├── Remove Executable Files.asset.meta
│ │ │ │ │ │ │ │ ├── Remove JPG Files.asset.meta
│ │ │ │ │ │ │ │ ├── Remove JavaScript Files.asset.meta
│ │ │ │ │ │ │ │ ├── Remove Lossy Audio Files.asset.meta
│ │ │ │ │ │ │ │ ├── Remove Mixamo Files.asset.meta
│ │ │ │ │ │ │ │ ├── Remove SpeedTree Files.asset.meta
│ │ │ │ │ │ │ │ └── Remove Video Files.asset.meta
│ │ │ │ │ │ │ ├── Generic.meta
│ │ │ │ │ │ │ ├── UnityPackage/
│ │ │ │ │ │ │ │ ├── Check Demo Scenes.asset.meta
│ │ │ │ │ │ │ │ ├── Check Documentation.asset.meta
│ │ │ │ │ │ │ │ ├── Check Package Size.asset.meta
│ │ │ │ │ │ │ │ └── Check Project Template Assets.asset.meta
│ │ │ │ │ │ │ └── UnityPackage.meta
│ │ │ │ │ │ └── Tests.meta
│ │ │ │ │ └── Validator.meta
│ │ │ │ ├── Editor.meta
│ │ │ │ ├── LICENSE.md
│ │ │ │ ├── LICENSE.md.meta
│ │ │ │ ├── package.json
│ │ │ │ └── package.json.meta
│ │ │ ├── manifest.json
│ │ │ └── packages-lock.json
│ │ └── ProjectSettings/
│ │ ├── BurstAotSettings_StandaloneWindows.json
│ │ ├── CommonBurstAotSettings.json
│ │ ├── ProjectVersion.txt
│ │ └── SceneTemplateSettings.json
│ └── UnityMCPTests/
│ ├── .gitignore
│ ├── Assets/
│ │ ├── Packages.meta
│ │ ├── Scenes/
│ │ │ ├── SampleScene.unity
│ │ │ ├── SampleScene.unity.meta
│ │ │ ├── Test.unity/
│ │ │ │ ├── Test.unity
│ │ │ │ └── Test.unity.meta
│ │ │ └── Test.unity.meta
│ │ ├── Scenes.meta
│ │ ├── Scripts/
│ │ │ ├── Bouncer.cs
│ │ │ ├── Bouncer.cs.meta
│ │ │ ├── Hello.cs
│ │ │ ├── Hello.cs.meta
│ │ │ ├── LongUnityScriptClaudeTest.cs
│ │ │ ├── LongUnityScriptClaudeTest.cs.meta
│ │ │ ├── TestAsmdef/
│ │ │ │ ├── CustomComponent.cs
│ │ │ │ ├── CustomComponent.cs.meta
│ │ │ │ ├── TestAsmdef.asmdef
│ │ │ │ ├── TestAsmdef.asmdef.meta
│ │ │ │ ├── UnityEventTestComponent.cs
│ │ │ │ └── UnityEventTestComponent.cs.meta
│ │ │ └── TestAsmdef.meta
│ │ ├── Scripts.meta
│ │ ├── Temp.meta
│ │ ├── TestMat.mat
│ │ ├── TestMat.mat.meta
│ │ ├── Tests/
│ │ │ ├── EditMode/
│ │ │ │ ├── Helpers/
│ │ │ │ │ ├── AssetPathUtilityOfflineTests.cs
│ │ │ │ │ ├── AssetPathUtilityOfflineTests.cs.meta
│ │ │ │ │ ├── CodexConfigHelperTests.cs
│ │ │ │ │ ├── CodexConfigHelperTests.cs.meta
│ │ │ │ │ ├── Matrix4x4ConverterTests.cs
│ │ │ │ │ ├── Matrix4x4ConverterTests.cs.meta
│ │ │ │ │ ├── PaginationTests.cs
│ │ │ │ │ ├── PaginationTests.cs.meta
│ │ │ │ │ ├── ToolParamsTests.cs
│ │ │ │ │ ├── ToolParamsTests.cs.meta
│ │ │ │ │ ├── WriteToConfigTests.cs
│ │ │ │ │ └── WriteToConfigTests.cs.meta
│ │ │ │ ├── Helpers.meta
│ │ │ │ ├── MCPForUnityTests.Editor.asmdef
│ │ │ │ ├── MCPForUnityTests.Editor.asmdef.meta
│ │ │ │ ├── Resources/
│ │ │ │ │ ├── GetMenuItemsTests.cs
│ │ │ │ │ └── GetMenuItemsTests.cs.meta
│ │ │ │ ├── Resources.meta
│ │ │ │ ├── Services/
│ │ │ │ │ ├── Characterization/
│ │ │ │ │ │ ├── ServerManagementServiceCharacterizationTests.cs
│ │ │ │ │ │ ├── ServerManagementServiceCharacterizationTests.cs.meta
│ │ │ │ │ │ ├── Services_Characterization.cs
│ │ │ │ │ │ └── Services_Characterization.cs.meta
│ │ │ │ │ ├── Characterization.meta
│ │ │ │ │ ├── EditorConfigurationCacheTests.cs
│ │ │ │ │ ├── EditorConfigurationCacheTests.cs.meta
│ │ │ │ │ ├── PackageUpdateServiceTests.cs
│ │ │ │ │ ├── PackageUpdateServiceTests.cs.meta
│ │ │ │ │ ├── PortManagerTests.cs
│ │ │ │ │ ├── PortManagerTests.cs.meta
│ │ │ │ │ ├── Server/
│ │ │ │ │ │ ├── PidFileManagerTests.cs
│ │ │ │ │ │ ├── PidFileManagerTests.cs.meta
│ │ │ │ │ │ ├── ProcessDetectorTests.cs
│ │ │ │ │ │ ├── ProcessDetectorTests.cs.meta
│ │ │ │ │ │ ├── ProcessTerminatorTests.cs
│ │ │ │ │ │ ├── ProcessTerminatorTests.cs.meta
│ │ │ │ │ │ ├── ServerCommandBuilderTests.cs
│ │ │ │ │ │ ├── ServerCommandBuilderTests.cs.meta
│ │ │ │ │ │ ├── TerminalLauncherTests.cs
│ │ │ │ │ │ └── TerminalLauncherTests.cs.meta
│ │ │ │ │ ├── Server.meta
│ │ │ │ │ ├── StdioBridgeReconnectTests.cs
│ │ │ │ │ ├── StdioBridgeReconnectTests.cs.meta
│ │ │ │ │ ├── ToolDiscoveryServiceTests.cs
│ │ │ │ │ ├── ToolDiscoveryServiceTests.cs.meta
│ │ │ │ │ ├── WebSocketTransportClientTests.cs
│ │ │ │ │ └── WebSocketTransportClientTests.cs.meta
│ │ │ │ ├── Services.meta
│ │ │ │ ├── TestUtilities.cs
│ │ │ │ ├── TestUtilities.cs.meta
│ │ │ │ ├── Tools/
│ │ │ │ │ ├── AIPropertyMatchingTests.cs
│ │ │ │ │ ├── AIPropertyMatchingTests.cs.meta
│ │ │ │ │ ├── BatchExecuteKeyPreservationTests.cs
│ │ │ │ │ ├── BatchExecuteKeyPreservationTests.cs.meta
│ │ │ │ │ ├── Characterization/
│ │ │ │ │ │ ├── EditorTools_Characterization.cs
│ │ │ │ │ │ └── EditorTools_Characterization.cs.meta
│ │ │ │ │ ├── Characterization.meta
│ │ │ │ │ ├── CommandRegistryTests.cs
│ │ │ │ │ ├── CommandRegistryTests.cs.meta
│ │ │ │ │ ├── ComponentOpsUnityEventTests.cs
│ │ │ │ │ ├── ComponentOpsUnityEventTests.cs.meta
│ │ │ │ │ ├── ComponentResolverTests.cs
│ │ │ │ │ ├── ComponentResolverTests.cs.meta
│ │ │ │ │ ├── DomainReloadResilienceTests.cs
│ │ │ │ │ ├── DomainReloadResilienceTests.cs.meta
│ │ │ │ │ ├── ExecuteMenuItemTests.cs
│ │ │ │ │ ├── ExecuteMenuItemTests.cs.meta
│ │ │ │ │ ├── Fixtures/
│ │ │ │ │ │ ├── ManageScriptableObjectTestDefinition.cs
│ │ │ │ │ │ ├── ManageScriptableObjectTestDefinition.cs.meta
│ │ │ │ │ │ ├── ManageScriptableObjectTestDefinitionBase.cs
│ │ │ │ │ │ ├── ManageScriptableObjectTestDefinitionBase.cs.meta
│ │ │ │ │ │ ├── StressTestSOs/
│ │ │ │ │ │ │ ├── ArrayStressSO.cs
│ │ │ │ │ │ │ ├── ArrayStressSO.cs.meta
│ │ │ │ │ │ │ ├── ComplexStressSO.cs
│ │ │ │ │ │ │ ├── ComplexStressSO.cs.meta
│ │ │ │ │ │ │ ├── DeepStressSO.cs
│ │ │ │ │ │ │ └── DeepStressSO.cs.meta
│ │ │ │ │ │ └── StressTestSOs.meta
│ │ │ │ │ ├── Fixtures.meta
│ │ │ │ │ ├── GameObjectAPIStressTests.cs
│ │ │ │ │ ├── GameObjectAPIStressTests.cs.meta
│ │ │ │ │ ├── GameObjectComponentHelpersErrorTests.cs
│ │ │ │ │ ├── GameObjectComponentHelpersErrorTests.cs.meta
│ │ │ │ │ ├── MCPToolParameterTests.cs
│ │ │ │ │ ├── MCPToolParameterTests.cs.meta
│ │ │ │ │ ├── ManageAnimationTests.cs
│ │ │ │ │ ├── ManageAnimationTests.cs.meta
│ │ │ │ │ ├── ManageGameObjectCreateTests.cs
│ │ │ │ │ ├── ManageGameObjectCreateTests.cs.meta
│ │ │ │ │ ├── ManageGameObjectDeleteTests.cs
│ │ │ │ │ ├── ManageGameObjectDeleteTests.cs.meta
│ │ │ │ │ ├── ManageGameObjectModifyTests.cs
│ │ │ │ │ ├── ManageGameObjectModifyTests.cs.meta
│ │ │ │ │ ├── ManageGameObjectTests.cs
│ │ │ │ │ ├── ManageGameObjectTests.cs.meta
│ │ │ │ │ ├── ManageGraphicsTests.cs
│ │ │ │ │ ├── ManageMaterialPropertiesTests.cs
│ │ │ │ │ ├── ManageMaterialPropertiesTests.cs.meta
│ │ │ │ │ ├── ManageMaterialReproTests.cs
│ │ │ │ │ ├── ManageMaterialReproTests.cs.meta
│ │ │ │ │ ├── ManageMaterialStressTests.cs
│ │ │ │ │ ├── ManageMaterialStressTests.cs.meta
│ │ │ │ │ ├── ManageMaterialTests.cs
│ │ │ │ │ ├── ManageMaterialTests.cs.meta
│ │ │ │ │ ├── ManagePrefabsCrudTests.cs
│ │ │ │ │ ├── ManagePrefabsCrudTests.cs.meta
│ │ │ │ │ ├── ManageProBuilderTests.cs
│ │ │ │ │ ├── ManageProBuilderTests.cs.meta
│ │ │ │ │ ├── ManageSceneHierarchyPagingTests.cs
│ │ │ │ │ ├── ManageSceneHierarchyPagingTests.cs.meta
│ │ │ │ │ ├── ManageScriptDelimiterTests.cs
│ │ │ │ │ ├── ManageScriptDelimiterTests.cs.meta
│ │ │ │ │ ├── ManageScriptValidationTests.cs
│ │ │ │ │ ├── ManageScriptValidationTests.cs.meta
│ │ │ │ │ ├── ManageScriptableObjectStressTests.cs
│ │ │ │ │ ├── ManageScriptableObjectStressTests.cs.meta
│ │ │ │ │ ├── ManageScriptableObjectTests.cs
│ │ │ │ │ ├── ManageScriptableObjectTests.cs.meta
│ │ │ │ │ ├── ManageUITests.cs
│ │ │ │ │ ├── ManageUITests.cs.meta
│ │ │ │ │ ├── MaterialDirectPropertiesTests.cs
│ │ │ │ │ ├── MaterialDirectPropertiesTests.cs.meta
│ │ │ │ │ ├── MaterialMeshInstantiationTests.cs
│ │ │ │ │ ├── MaterialMeshInstantiationTests.cs.meta
│ │ │ │ │ ├── MaterialParameterToolTests.cs
│ │ │ │ │ ├── MaterialParameterToolTests.cs.meta
│ │ │ │ │ ├── PropertyConversionErrorHandlingTests.cs
│ │ │ │ │ ├── PropertyConversionErrorHandlingTests.cs.meta
│ │ │ │ │ ├── PropertyConversion_ArrayForFloat_Test.cs
│ │ │ │ │ ├── PropertyConversion_ArrayForFloat_Test.cs.meta
│ │ │ │ │ ├── ReadConsoleTests.cs
│ │ │ │ │ ├── ReadConsoleTests.cs.meta
│ │ │ │ │ ├── RunTestsTests.cs
│ │ │ │ │ ├── RunTestsTests.cs.meta
│ │ │ │ │ ├── UIDocumentSerializationTests.cs
│ │ │ │ │ ├── UIDocumentSerializationTests.cs.meta
│ │ │ │ │ ├── UnityReflectTests.cs
│ │ │ │ │ └── UnityReflectTests.cs.meta
│ │ │ │ ├── Tools.meta
│ │ │ │ ├── Windows/
│ │ │ │ │ ├── Characterization/
│ │ │ │ │ │ ├── Windows_Characterization.cs
│ │ │ │ │ │ └── Windows_Characterization.cs.meta
│ │ │ │ │ └── Characterization.meta
│ │ │ │ └── Windows.meta
│ │ │ ├── EditMode.meta
│ │ │ ├── Editor.meta
│ │ │ ├── PlayMode/
│ │ │ │ ├── MCPForUnityTests.PlayMode.asmdef
│ │ │ │ ├── MCPForUnityTests.PlayMode.asmdef.meta
│ │ │ │ ├── PlayModeBasicTests.cs
│ │ │ │ └── PlayModeBasicTests.cs.meta
│ │ │ └── PlayMode.meta
│ │ ├── Tests.meta
│ │ ├── UI Toolkit/
│ │ │ ├── UnityThemes/
│ │ │ │ ├── UnityDefaultRuntimeTheme.tss
│ │ │ │ └── UnityDefaultRuntimeTheme.tss.meta
│ │ │ └── UnityThemes.meta
│ │ └── UI Toolkit.meta
│ ├── Packages/
│ │ └── manifest.json
│ └── ProjectSettings/
│ ├── Packages/
│ │ └── com.unity.testtools.codecoverage/
│ │ └── Settings.json
│ ├── ProjectVersion.txt
│ └── SceneTemplateSettings.json
├── docker-compose.yml
├── docs/
│ ├── development/
│ │ ├── README-DEV-zh.md
│ │ └── README-DEV.md
│ ├── feature-roadmap-2026.md
│ ├── guides/
│ │ ├── CLI_EXAMPLE.md
│ │ ├── CLI_USAGE.md
│ │ ├── CURSOR_HELP.md
│ │ ├── MCP_CLIENT_CONFIGURATORS.md
│ │ ├── RELEASING.md
│ │ └── REMOTE_SERVER_AUTH.md
│ ├── i18n/
│ │ └── README-zh.md
│ ├── migrations/
│ │ ├── v5_MIGRATION.md
│ │ ├── v6_NEW_UI_CHANGES.md
│ │ └── v8_NEW_NETWORKING_SETUP.md
│ └── reference/
│ ├── CUSTOM_TOOLS.md
│ ├── REMOTE_SERVER_AUTH_ARCHITECTURE.md
│ └── TELEMETRY.md
├── manifest.json
├── mcp_source.py
├── scripts/
│ └── validate-nlt-coverage.sh
├── tools/
│ ├── UPDATE_DOCS_PROMPT.md
│ ├── docker_publish.sh
│ ├── generate_mcpb.py
│ ├── prepare_unity_asset_store_release.py
│ ├── pypi_publish.sh
│ ├── stress_editor_state.py
│ ├── stress_mcp.py
│ ├── tests/
│ │ ├── __init__.py
│ │ └── test_build_release_characterization.py
│ ├── update_fork.bat
│ ├── update_fork.sh
│ └── update_versions.py
└── unity-mcp-skill/
├── SKILL.md
└── references/
├── probuilder-guide.md
├── resources-reference.md
├── tools-reference.md
└── workflows.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
Server/build
.git
.venv
__pycache__
*.pyc
.DS_Store
================================================
FILE: .github/actions/publish-docker/action.yml
================================================
name: Publish Docker image
description: Build and push the Docker image to Docker Hub
inputs:
docker_username:
required: true
description: Docker Hub username
docker_password:
required: true
description: Docker Hub password
image:
required: true
description: Docker image name (e.g. user/repo)
version:
required: false
default: ""
description: Optional version string (e.g. 1.2.3). If provided, tags are computed from this version instead of the GitHub ref.
include_branch_tags:
required: false
default: "true"
description: Whether to also publish a branch tag when running on a branch ref (e.g. manual runs).
context:
required: false
default: .
description: Docker build context
dockerfile:
required: false
default: Server/Dockerfile
description: Path to Dockerfile
platforms:
required: false
default: linux/amd64
description: Target platforms
runs:
using: composite
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ inputs.docker_username }}
password: ${{ inputs.docker_password }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
if: ${{ inputs.version == '' && inputs.include_branch_tags == 'true' }}
with:
images: ${{ inputs.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
- name: Extract metadata (tags, labels) for Docker
id: meta_nobranch
uses: docker/metadata-action@v5
if: ${{ inputs.version == '' && inputs.include_branch_tags != 'true' }}
with:
images: ${{ inputs.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Compute Docker tags from version
id: version_tags
if: ${{ inputs.version != '' }}
shell: bash
run: |
set -euo pipefail
IFS='.' read -r MA MI PA <<< "${{ inputs.version }}"
echo "major=$MA" >> "$GITHUB_OUTPUT"
echo "minor=$MI" >> "$GITHUB_OUTPUT"
- name: Extract metadata (tags, labels) for Docker
id: meta_version
uses: docker/metadata-action@v5
if: ${{ inputs.version != '' && inputs.include_branch_tags == 'true' }}
with:
images: ${{ inputs.image }}
tags: |
type=raw,value=v${{ inputs.version }}
type=raw,value=v${{ steps.version_tags.outputs.major }}.${{ steps.version_tags.outputs.minor }}
type=raw,value=v${{ steps.version_tags.outputs.major }}
type=ref,event=branch
- name: Extract metadata (tags, labels) for Docker
id: meta_version_nobranch
uses: docker/metadata-action@v5
if: ${{ inputs.version != '' && inputs.include_branch_tags != 'true' }}
with:
images: ${{ inputs.image }}
tags: |
type=raw,value=v${{ inputs.version }}
type=raw,value=v${{ steps.version_tags.outputs.major }}.${{ steps.version_tags.outputs.minor }}
type=raw,value=v${{ steps.version_tags.outputs.major }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: ${{ inputs.platforms }}
push: true
tags: ${{ steps.meta.outputs.tags || steps.meta_nobranch.outputs.tags || steps.meta_version.outputs.tags || steps.meta_version_nobranch.outputs.tags }}
labels: ${{ steps.meta.outputs.labels || steps.meta_nobranch.outputs.labels || steps.meta_version.outputs.labels || steps.meta_version_nobranch.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .github/actions/publish-pypi/action.yml
================================================
name: Publish Python distribution to PyPI
description: Build and publish the Python package from Server/ to PyPI
runs:
using: composite
steps:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "Server/uv.lock"
- name: Build a binary wheel and a source tarball
shell: bash
run: uv build
working-directory: ./Server
- name: Publish distribution to PyPI
# Pin to v1.12.4 to avoid Docker container name issue with uppercase repo names in v1.13.0+
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
packages-dir: Server/dist/
================================================
FILE: .github/pull_request_template.md
================================================
## Description
## Type of Change
Save your change type
- Bug fix (non-breaking change that fixes an issue)
- New feature (non-breaking change that adds functionality)
- Breaking change (fix or feature that would cause existing functionality to change)
- Documentation update
- Refactoring (no functional changes)
- Test update
## Changes Made
## Testing/Screenshots/Recordings
## Documentation Updates
- [ ] I have added/removed/modified tools or resources
- [ ] If yes, I have updated all documentation files using:
- [ ] The LLM prompt at `tools/UPDATE_DOCS_PROMPT.md` (recommended)
- [ ] Manual updates following the guide at `tools/UPDATE_DOCS.md`
## Related Issues
## Additional Notes
================================================
FILE: .github/scripts/mark_skipped.py
================================================
#!/usr/bin/env python3
"""
Post-processes a JUnit XML so that "expected"/environmental failures
(e.g., permission prompts, empty MCP resources, or schema hiccups)
are converted to . Leaves real failures intact.
Usage:
python .github/scripts/mark_skipped.py reports/claude-nl-tests.xml
"""
from __future__ import annotations
import sys
import os
import re
import xml.etree.ElementTree as ET
PATTERNS = [
r"\bpermission\b",
r"\bpermissions\b",
r"\bautoApprove\b",
r"\bapproval\b",
r"\bdenied\b",
r"requested\s+permissions",
r"^MCP resources list is empty$",
r"No MCP resources detected",
r"aggregator.*returned\s*\[\s*\]",
r"Unknown resource:\s*mcpforunity://",
r"Input should be a valid dictionary.*ctx",
r"validation error .* ctx",
]
def should_skip(msg: str) -> bool:
if not msg:
return False
msg_l = msg.strip()
for pat in PATTERNS:
if re.search(pat, msg_l, flags=re.IGNORECASE | re.MULTILINE):
return True
return False
def summarize_counts(ts: ET.Element):
tests = 0
failures = 0
errors = 0
skipped = 0
for case in ts.findall("testcase"):
tests += 1
if case.find("failure") is not None:
failures += 1
if case.find("error") is not None:
errors += 1
if case.find("skipped") is not None:
skipped += 1
return tests, failures, errors, skipped
def main(path: str) -> int:
if not os.path.exists(path):
print(f"[mark_skipped] No JUnit at {path}; nothing to do.")
return 0
try:
tree = ET.parse(path)
except ET.ParseError as e:
print(f"[mark_skipped] Could not parse {path}: {e}")
return 0
root = tree.getroot()
suites = root.findall("testsuite") if root.tag == "testsuites" else [root]
changed = False
for ts in suites:
for case in list(ts.findall("testcase")):
nodes = [n for n in list(case) if n.tag in ("failure", "error")]
if not nodes:
continue
# If any node matches skip patterns, convert the whole case to skipped.
first_match_text = None
to_skip = False
for n in nodes:
msg = (n.get("message") or "") + "\n" + (n.text or "")
if should_skip(msg):
first_match_text = (
n.text or "").strip() or first_match_text
to_skip = True
if to_skip:
for n in nodes:
case.remove(n)
reason = "Marked skipped: environment/permission precondition not met"
skip = ET.SubElement(case, "skipped")
skip.set("message", reason)
skip.text = first_match_text or reason
changed = True
# Recompute tallies per testsuite
tests, failures, errors, skipped = summarize_counts(ts)
ts.set("tests", str(tests))
ts.set("failures", str(failures))
ts.set("errors", str(errors))
ts.set("skipped", str(skipped))
if changed:
tree.write(path, encoding="utf-8", xml_declaration=True)
print(
f"[mark_skipped] Updated {path}: converted environmental failures to skipped.")
else:
print(f"[mark_skipped] No environmental failures detected in {path}.")
return 0
if __name__ == "__main__":
target = (
sys.argv[1]
if len(sys.argv) > 1
else os.environ.get("JUNIT_OUT", "reports/junit-nl-suite.xml")
)
raise SystemExit(main(target))
================================================
FILE: .github/workflows/beta-release.yml
================================================
name: Beta Release (PyPI Pre-release)
concurrency:
group: beta-release
cancel-in-progress: true
on:
push:
branches:
- beta
paths:
- "Server/**"
- "MCPForUnity/**"
jobs:
update_unity_beta_version:
name: Update Unity package to beta version
runs-on: ubuntu-latest
# Avoid running when the workflow's own automation merges the PR
# created by this workflow (prevents a version-bump loop).
if: github.actor != 'github-actions[bot]'
permissions:
contents: write
pull-requests: write
outputs:
unity_beta_version: ${{ steps.version.outputs.unity_beta_version }}
version_updated: ${{ steps.commit.outputs.updated }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: beta
- name: Generate beta version for Unity package
id: version
shell: bash
run: |
set -euo pipefail
# Read current Unity package version
CURRENT_VERSION=$(jq -r '.version' MCPForUnity/package.json)
echo "Current Unity package version: $CURRENT_VERSION"
# Check if already a beta version - increment beta number
if [[ "$CURRENT_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-beta\.([0-9]+)$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
BETA_NUM="${BASH_REMATCH[2]}"
NEXT_BETA=$((BETA_NUM + 1))
BETA_VERSION="${BASE_VERSION}-beta.${NEXT_BETA}"
echo "Incrementing beta number: $CURRENT_VERSION -> $BETA_VERSION"
elif [[ "$CURRENT_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
# Stable version - bump patch and add -beta.1 suffix
# This ensures beta is "newer" than stable (9.3.2-beta.1 > 9.3.1)
# The release workflow decides final bump type (patch/minor/major)
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PATCH="${BASH_REMATCH[3]}"
NEXT_PATCH=$((PATCH + 1))
BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.1"
echo "Converting stable to beta: $CURRENT_VERSION -> $BETA_VERSION"
else
echo "Error: Could not parse version '$CURRENT_VERSION'" >&2
exit 1
fi
# Always output the computed version
echo "unity_beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT"
# Only skip update if computed version matches current (no change needed)
if [[ "$BETA_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version unchanged, skipping update"
echo "needs_update=false" >> "$GITHUB_OUTPUT"
else
echo "Version will be updated: $CURRENT_VERSION -> $BETA_VERSION"
echo "needs_update=true" >> "$GITHUB_OUTPUT"
fi
- name: Update Unity package.json with beta version
if: steps.version.outputs.needs_update == 'true'
env:
BETA_VERSION: ${{ steps.version.outputs.unity_beta_version }}
shell: bash
run: |
set -euo pipefail
# Update package.json version
jq --arg v "$BETA_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
mv tmp.json MCPForUnity/package.json
echo "Updated MCPForUnity/package.json:"
jq '.version' MCPForUnity/package.json
- name: Commit to temporary branch and create PR
id: commit
if: steps.version.outputs.needs_update == 'true'
env:
GH_TOKEN: ${{ github.token }}
BETA_VERSION: ${{ steps.version.outputs.unity_beta_version }}
shell: bash
run: |
set -euo pipefail
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
if git diff --quiet MCPForUnity/package.json; then
echo "No changes to commit"
echo "updated=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Create a temporary branch for the version update
BRANCH="beta-version-${BETA_VERSION}-${GITHUB_RUN_ID}"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
git checkout -b "$BRANCH"
git add MCPForUnity/package.json
git commit -m "chore: update Unity package to beta version ${BETA_VERSION}"
git push origin "$BRANCH"
# Check if PR already exists
if gh pr view "$BRANCH" >/dev/null 2>&1; then
echo "PR already exists for $BRANCH"
PR_NUMBER=$(gh pr view "$BRANCH" --json number -q '.number')
else
PR_URL=$(gh pr create \
--base beta \
--head "$BRANCH" \
--title "chore: update Unity package to beta version ${BETA_VERSION}" \
--body "Automated beta version bump for the Unity package.")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
fi
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "updated=true" >> "$GITHUB_OUTPUT"
- name: Auto-merge version bump PR
if: steps.commit.outputs.updated == 'true' && steps.commit.outputs.pr_number != ''
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.commit.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
gh pr merge "$PR_NUMBER" --merge --delete-branch
publish_pypi_prerelease:
name: Publish beta to PyPI (pre-release)
runs-on: ubuntu-latest
# Avoid double-publish when the bot merges the version bump PR
if: github.actor != 'github-actions[bot]'
environment:
name: pypi
url: https://pypi.org/p/mcpforunityserver
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: beta
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "Server/uv.lock"
- name: Generate beta version
id: version
shell: bash
run: |
set -euo pipefail
RAW_VERSION=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
echo "Raw version: $RAW_VERSION"
# Check if already a beta/prerelease version
if [[ "$RAW_VERSION" =~ (a|b|rc|\.dev|\.post)[0-9]+$ ]]; then
IS_PRERELEASE=true
# Strip the prerelease suffix to get base version
BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/(a|b|rc|\.dev|\.post)[0-9]+$//')
else
IS_PRERELEASE=false
BASE_VERSION="$RAW_VERSION"
fi
# Validate we have a proper X.Y.Z format
if ! [[ "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Could not parse version '$RAW_VERSION' -> '$BASE_VERSION'" >&2
exit 1
fi
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
# Only bump patch if coming from stable; keep same base if already prerelease
if [[ "$IS_PRERELEASE" == "true" ]]; then
# Already on a beta series - keep the same base version
NEXT_PATCH="$PATCH"
echo "Already prerelease, keeping base: $BASE_VERSION"
else
# Stable version - bump patch to ensure beta is "newer"
NEXT_PATCH=$((PATCH + 1))
echo "Stable version, bumping patch: $PATCH -> $NEXT_PATCH"
fi
BETA_NUMBER="$(date +%Y%m%d%H%M%S)"
BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}b${BETA_NUMBER}"
echo "Base version: $BASE_VERSION"
echo "Beta version: $BETA_VERSION"
echo "beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT"
- name: Update version for beta release
env:
BETA_VERSION: ${{ steps.version.outputs.beta_version }}
shell: bash
run: |
set -euo pipefail
sed -i "s/^version = .*/version = \"${BETA_VERSION}\"/" Server/pyproject.toml
echo "Updated pyproject.toml:"
grep "^version" Server/pyproject.toml
- name: Build a binary wheel and a source tarball
shell: bash
run: uv build
working-directory: ./Server
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: Server/dist/
================================================
FILE: .github/workflows/claude-nl-suite.yml
================================================
name: Claude NL/T Full Suite (Unity live)
on: [workflow_dispatch]
permissions:
contents: read
checks: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f2-linux-il2cpp-3
jobs:
nl-suite:
runs-on: ubuntu-24.04
timeout-minutes: 60
env:
JUNIT_OUT: reports/junit-nl-suite.xml
MD_OUT: reports/junit-nl-suite.md
steps:
# ---------- Secrets check ----------
- name: Detect secrets (outputs)
id: detect
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set -e
if [ -n "$ANTHROPIC_API_KEY" ]; then echo "anthropic_ok=true" >> "$GITHUB_OUTPUT"; else echo "anthropic_ok=false" >> "$GITHUB_OUTPUT"; fi
if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then
echo "unity_ok=true" >> "$GITHUB_OUTPUT"
else
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
with:
fetch-depth: 0
# ---------- Python env for MCP server (uv) ----------
- uses: astral-sh/setup-uv@v4
with:
python-version: "3.11"
- name: Install MCP server
run: |
set -eux
uv venv
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV"
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
if [ -f Server/pyproject.toml ]; then
uv pip install -e Server
elif [ -f Server/requirements.txt ]; then
uv pip install -r Server/requirements.txt
else
echo "No MCP Python deps found (skipping)"
fi
# --- Licensing: allow both ULF and EBL when available ---
- name: Decide license sources
id: lic
shell: bash
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -eu
use_ulf=false; use_ebl=false
[[ -n "${UNITY_LICENSE:-}" ]] && use_ulf=true
[[ -n "${UNITY_EMAIL:-}" && -n "${UNITY_PASSWORD:-}" && -n "${UNITY_SERIAL:-}" ]] && use_ebl=true
echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT"
echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT"
echo "has_serial=$([[ -n "${UNITY_SERIAL:-}" ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
- name: Stage Unity .ulf license (from secret)
if: steps.lic.outputs.use_ulf == 'true'
id: ulf
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
shell: bash
run: |
set -eu
mkdir -p "$RUNNER_TEMP/unity-license-ulf" "$RUNNER_TEMP/unity-local/Unity"
f="$RUNNER_TEMP/unity-license-ulf/Unity_lic.ulf"
if printf "%s" "$UNITY_LICENSE" | base64 -d - >/dev/null 2>&1; then
printf "%s" "$UNITY_LICENSE" | base64 -d - > "$f"
else
printf "%s" "$UNITY_LICENSE" > "$f"
fi
chmod 600 "$f" || true
# Detect ULF first; it is XML and includes a element.
if grep -qi '' "$f"; then
# provide it in the standard local-share path too
cp -f "$f" "$RUNNER_TEMP/unity-local/Unity/Unity_lic.ulf"
echo "License source: ULF (Signature found)"
echo "ok=true" >> "$GITHUB_OUTPUT"
# If someone pasted an entitlement XML into UNITY_LICENSE by mistake, re-home it:
elif grep -qi 'Entitlement|entitlement' "$f"; then
mkdir -p "$RUNNER_TEMP/unity-config/Unity/licenses"
mv "$f" "$RUNNER_TEMP/unity-config/Unity/licenses/UnityEntitlementLicense.xml"
echo "License source: Entitlement XML (re-homed)"
echo "ok=false" >> "$GITHUB_OUTPUT"
else
echo "License source: Unknown format (no ULF Signature or Entitlement markers)"
echo "ok=false" >> "$GITHUB_OUTPUT"
fi
# --- Activate via EBL inside the same Unity image (writes host-side entitlement) ---
- name: Activate Unity (EBL via container - host-mount)
if: steps.lic.outputs.use_ebl == 'true'
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -euo pipefail
# host dirs to receive the full Unity config and local-share
mkdir -p "$RUNNER_TEMP/unity-config" "$RUNNER_TEMP/unity-local"
# Try Pro first if serial is present, otherwise named-user EBL.
docker run --rm --network host \
-e HOME=/root \
-e UNITY_EMAIL -e UNITY_PASSWORD -e UNITY_SERIAL \
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
"$UNITY_IMAGE" bash -lc '
set -euxo pipefail
if [[ -n "${UNITY_SERIAL:-}" ]]; then
/opt/unity/Editor/Unity -batchmode -nographics -logFile - \
-username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true
else
/opt/unity/Editor/Unity -batchmode -nographics -logFile - \
-username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -quit || true
fi
ls -la /root/.config/unity3d/Unity/licenses || true
'
# Verify entitlement written to host mount; allow ULF-only runs to proceed
if ! find "$RUNNER_TEMP/unity-config" -type f -iname "*.xml" | grep -q .; then
if [[ "${{ steps.ulf.outputs.ok }}" == "true" ]]; then
echo "EBL entitlement not found; proceeding with ULF-only (ok=true)."
else
echo "No entitlement produced and no valid ULF; cannot continue." >&2
exit 1
fi
fi
# EBL entitlement is already written directly to $RUNNER_TEMP/unity-config by the activation step
# ---------- Warm up project (import Library once) ----------
- name: Warm up project (import Library once)
if: steps.detect.outputs.anthropic_ok == 'true' && (steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true')
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
ULF_OK: ${{ steps.ulf.outputs.ok }}
run: |
set -euxo pipefail
manual_args=()
if [[ "${ULF_OK:-false}" == "true" ]]; then
manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
fi
docker run --rm --network host \
-e HOME=/root \
-v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" \
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
-v "$RUNNER_TEMP/unity-cache:/root/.cache/unity3d" \
"$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \
-projectPath "${{ github.workspace }}/TestProjects/UnityMCPTests" \
"${manual_args[@]}" \
-quit
# ---------- Clean old MCP status ----------
- name: Clean old MCP status
run: |
set -eux
mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
rm -f "$GITHUB_WORKSPACE/.unity-mcp"/unity-mcp-status-*.json || true
# ---------- Start headless Unity (persistent bridge) ----------
- name: Start Unity (persistent bridge)
if: steps.detect.outputs.anthropic_ok == 'true' && (steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true')
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
ULF_OK: ${{ steps.ulf.outputs.ok }}
run: |
set -euxo pipefail
manual_args=()
if [[ "${ULF_OK:-false}" == "true" ]]; then
manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
fi
mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
docker rm -f unity-mcp >/dev/null 2>&1 || true
docker run -d --name unity-mcp --network host \
-e HOME=/root \
-e UNITY_MCP_ALLOW_BATCH=1 \
-e UNITY_MCP_STATUS_DIR="${{ github.workspace }}/.unity-mcp" \
-e UNITY_MCP_BIND_HOST=127.0.0.1 \
-v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" \
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
-v "$RUNNER_TEMP/unity-cache:/root/.cache/unity3d" \
"$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile /root/.config/unity3d/Editor.log \
-stackTraceLogType Full \
-projectPath "${{ github.workspace }}/TestProjects/UnityMCPTests" \
"${manual_args[@]}" \
-executeMethod MCPForUnity.Editor.McpCiBoot.StartStdioForCi
# ---------- Wait for Unity bridge ----------
- name: Wait for Unity bridge (robust)
if: steps.detect.outputs.anthropic_ok == 'true' && (steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true')
shell: bash
run: |
set -euo pipefail
deadline=$((SECONDS+600)) # 10 min max
fatal_after=$((SECONDS+120)) # give licensing 2 min to settle
# Fail fast only if container actually died
st="$(docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' unity-mcp 2>/dev/null || true)"
case "$st" in exited*|dead*) docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'; exit 1;; esac
# Patterns
ok_pat='(Bridge|MCP(For)?Unity|AutoConnect).*(listening|ready|started|port|bound)'
# Only truly fatal signals; allow transient "Licensing::..." chatter
license_fatal='No valid Unity|License is not active|cannot load ULF|Signature element not found|Token not found|0 entitlement|Entitlement.*(failed|denied)|License (activation|return|renewal).*(failed|expired|denied)'
while [ $SECONDS -lt $deadline ]; do
logs="$(docker logs unity-mcp 2>&1 || true)"
# 1) Primary: status JSON exposes TCP port
port="$(jq -r '.unity_port // empty' "$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json 2>/dev/null | head -n1 || true)"
if [[ -n "${port:-}" ]] && timeout 1 bash -lc "exec 3<>/dev/tcp/127.0.0.1/$port"; then
echo "Bridge ready on port $port"
# Ensure status file is readable by all (Claude container might run as different user)
docker exec unity-mcp chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp" || chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp" || true
exit 0
fi
# 2) Secondary: log markers
if echo "$logs" | grep -qiE "$ok_pat"; then
echo "Bridge ready (log markers)"
docker exec unity-mcp chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp" || chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp" || true
exit 0
fi
# Only treat license failures as fatal *after* warm-up
if [ $SECONDS -ge $fatal_after ] && echo "$logs" | grep -qiE "$license_fatal"; then
echo "::error::Fatal licensing signal detected after warm-up"
echo "$logs" | tail -n 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'
exit 1
fi
# If the container dies mid-wait, bail
st="$(docker inspect -f '{{.State.Status}}' unity-mcp 2>/dev/null || true)"
if [[ "$st" != "running" ]]; then
echo "::error::Unity container exited during wait"; docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'
exit 1
fi
sleep 2
done
echo "::error::Bridge not ready before deadline"
docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'
exit 1
# ---------- Debug Unity bridge status ----------
- name: Debug Unity bridge status
if: always() && (steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true')
shell: bash
run: |
set -euxo pipefail
echo "--- Unity container state ---"
docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' unity-mcp || true
echo "--- Unity container logs (tail 200) ---"
docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' || true
echo "--- Container status dir ---"
docker exec unity-mcp ls -la "${{ github.workspace }}/.unity-mcp" || true
echo "--- Host status dir ---"
ls -la "$GITHUB_WORKSPACE/.unity-mcp" || true
echo "--- Host status file (first 120 lines) ---"
jq -r . "$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json | sed -n '1,120p' || true
echo "--- Port probe from host ---"
port="$(jq -r '.unity_port // empty' "$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json 2>/dev/null | head -n1 || true)"
echo "unity_port=${port:-}"
if [[ -n "${port:-}" ]]; then
timeout 1 bash -lc "exec 3<>/dev/tcp/127.0.0.1/$port" && echo "TCP OK" || echo "TCP probe failed"
else
echo "No unity_port in status file"
fi
echo "--- Config dir listing ---"
docker exec unity-mcp ls -la /root/.config/unity3d || true
echo "--- Editor log tail ---"
docker exec unity-mcp tail -n 200 /root/.config/unity3d/Editor.log || true
# Fail fast if no status file was written
shopt -s nullglob
status_files=("$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json)
if ((${#status_files[@]} == 0)); then
echo "::error::No Unity MCP status file found; failing fast."
exit 1
fi
# (moved) — return license after Unity is stopped
- name: Pin Claude tool permissions (.claude/settings.json)
run: |
set -eux
mkdir -p .claude
cat > .claude/settings.json <<'JSON'
{
"permissions": {
"allow": [
"mcp__unity",
"Edit(reports/**)",
"MultiEdit(reports/**)"
],
"deny": [
"Bash",
"WebFetch",
"WebSearch",
"Task",
"TodoWrite",
"NotebookEdit",
"NotebookRead"
]
}
}
JSON
# ---------- Reports & helper ----------
- name: Prepare reports and dirs
run: |
set -eux
rm -f reports/*.xml reports/*.md || true
mkdir -p reports reports/_snapshots reports/_staging
- name: Create report skeletons
run: |
set -eu
cat > "$JUNIT_OUT" <<'XML'
Bootstrap placeholder; suite will append real tests.
XML
printf '# Unity NL/T Editing Suite Test Results\n\n' > "$MD_OUT"
- name: Verify Unity bridge status/port
run: |
set -euxo pipefail
ls -la "$GITHUB_WORKSPACE/.unity-mcp" || true
jq -r . "$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json | sed -n '1,80p' || true
shopt -s nullglob
status_files=("$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json)
if ((${#status_files[@]})); then
port="$(grep -hEo '"unity_port"[[:space:]]*:[[:space:]]*[0-9]+' "${status_files[@]}" \
| sed -E 's/.*: *([0-9]+).*/\1/' | head -n1 || true)"
else
port=""
fi
echo "unity_port=$port"
if [[ -n "$port" ]]; then
timeout 1 bash -lc "exec 3<>/dev/tcp/127.0.0.1/$port" && echo "TCP OK"
fi
if ((${#status_files[@]})); then
first_status="${status_files[0]}"
fname="$(basename "$first_status")"
hash_part="${fname%.json}"; hash_part="${hash_part#unity-mcp-status-}"
proj="$(jq -r '.project_name // empty' "$first_status" || true)"
if [[ -n "${proj:-}" && -n "${hash_part:-}" ]]; then
echo "UNITY_MCP_DEFAULT_INSTANCE=${proj}@${hash_part}" >> "$GITHUB_ENV"
echo "Default instance set to ${proj}@${hash_part}"
fi
fi
# ---------- MCP client config ----------
- name: Write MCP config (.claude/mcp.json)
run: |
set -eux
mkdir -p .claude
python3 - <<'PY'
import json
import os
import textwrap
from pathlib import Path
workspace = os.environ["GITHUB_WORKSPACE"]
default_inst = os.environ.get("UNITY_MCP_DEFAULT_INSTANCE", "").strip()
cfg = {
"mcpServers": {
"unity": {
"args": [
"run",
"--active",
"--directory",
"Server",
"mcp-for-unity",
"--transport",
"stdio",
],
"transport": {"type": "stdio"},
"env": {
"PYTHONUNBUFFERED": "1",
"MCP_LOG_LEVEL": "debug",
"UNITY_PROJECT_ROOT": f"{workspace}/TestProjects/UnityMCPTests",
"UNITY_MCP_STATUS_DIR": f"{workspace}/.unity-mcp",
"UNITY_MCP_HOST": "127.0.0.1",
},
}
}
}
unity = cfg["mcpServers"]["unity"]
if default_inst:
unity["env"]["UNITY_MCP_DEFAULT_INSTANCE"] = default_inst
if "--default-instance" not in unity["args"]:
unity["args"] += ["--default-instance", default_inst]
runner_script = Path(".claude/run-unity-mcp.sh")
workspace_path = Path(workspace)
uv_candidate = workspace_path / ".venv" / "bin" / "uv"
uv_cmd = uv_candidate.as_posix() if uv_candidate.exists() else "uv"
script = textwrap.dedent(f"""\
#!/usr/bin/env bash
set -euo pipefail
LOG="{workspace}/.unity-mcp/mcp-server-startup-debug.log"
mkdir -p "$(dirname "$LOG")"
echo "" >> "$LOG"
echo "[ $(date -Iseconds) ] Starting unity MCP server" >> "$LOG"
# Redirect stderr to log, keep stdout for MCP communication
exec {uv_cmd} "$@" 2>> "$LOG"
""")
runner_script.write_text(script)
runner_script.chmod(0o755)
unity["command"] = runner_script.resolve().as_posix()
path = Path(".claude/mcp.json")
path.write_text(json.dumps(cfg, indent=2) + "\n")
print(f"Wrote {path} and {runner_script} (UNITY_MCP_DEFAULT_INSTANCE={default_inst or 'unset'})")
PY
- name: Debug MCP config
run: |
set -eux
echo "=== .claude/mcp.json ==="
cat .claude/mcp.json
echo ""
echo "=== Status dir contents ==="
ls -la "$GITHUB_WORKSPACE/.unity-mcp" || true
echo ""
echo "=== Status file content ==="
cat "$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json 2>/dev/null || echo "(no status files)"
- name: Preflight MCP server (with retries)
env:
UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
run: |
set -euxo pipefail
export PYTHONUNBUFFERED=1
export MCP_LOG_LEVEL=debug
export UNITY_PROJECT_ROOT="$GITHUB_WORKSPACE/TestProjects/UnityMCPTests"
export UNITY_MCP_STATUS_DIR="$GITHUB_WORKSPACE/.unity-mcp"
export UNITY_MCP_HOST=127.0.0.1
if [[ -n "${UNITY_MCP_DEFAULT_INSTANCE:-}" ]]; then
export UNITY_MCP_DEFAULT_INSTANCE
fi
# Debug: probe Unity's actual ping/pong response
echo "--- Unity ping/pong probe ---"
python3 <<'PY'
import socket, struct, sys
port = 6400
try:
s = socket.create_connection(("127.0.0.1", port), timeout=2)
s.settimeout(2)
hs = s.recv(512)
print(f"handshake: {hs!r}")
hs_ok = b"FRAMING=1" in hs
print(f"FRAMING=1 present: {hs_ok}")
if hs_ok:
s.sendall(struct.pack(">Q", 4) + b"ping")
hdr = s.recv(8)
print(f"response header len: {len(hdr)}")
if len(hdr) == 8:
length = struct.unpack(">Q", hdr)[0]
resp = s.recv(length)
print(f"response payload: {resp!r}")
pong_check = b'"message":"pong"'
print(f"contains pong_check: {pong_check in resp}")
s.close()
except Exception as e:
print(f"probe error: {e}")
PY
attempt=0
while true; do
attempt=$((attempt+1))
if uv run --active --directory Server python <<'PY' > /tmp/mcp-preflight.log 2>&1
import json
import os
import sys
sys.path.insert(0, "src")
from transport.legacy.unity_connection import send_command_with_retry
unity_instance = (os.environ.get("UNITY_MCP_DEFAULT_INSTANCE") or "").strip() or None
resp = send_command_with_retry(
"read_console",
{"action": "get", "count": "1", "include_stacktrace": False},
instance_id=unity_instance,
max_retries=1,
retry_ms=200,
retry_on_reload=False,
)
print(json.dumps(resp, default=str))
ok = isinstance(resp, dict) and (resp.get("success") is True or resp.get("status") == "success")
raise SystemExit(0 if ok else 1)
PY
then
echo "MCP command-plane preflight passed on attempt ${attempt}"
cat /tmp/mcp-preflight.log
break
fi
echo "MCP command-plane preflight failed on attempt ${attempt}"
cat /tmp/mcp-preflight.log || true
if [ "$attempt" -ge 8 ]; then
echo "::error::Unity command plane not ready after $attempt attempts"
cat /tmp/mcp-preflight.log || true
exit 1
fi
sleep 2
done
# ---------- Readiness diagnostics (only if preflight fails) ----------
- name: Readiness diagnostics (on preflight failure)
if: failure()
continue-on-error: true
env:
UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
run: |
set -euxo pipefail
export PYTHONUNBUFFERED=1 MCP_LOG_LEVEL=debug
export UNITY_PROJECT_ROOT="$GITHUB_WORKSPACE/TestProjects/UnityMCPTests"
export UNITY_MCP_STATUS_DIR="$GITHUB_WORKSPACE/.unity-mcp"
export UNITY_MCP_HOST=127.0.0.1
if [[ -n "${UNITY_MCP_DEFAULT_INSTANCE:-}" ]]; then
export UNITY_MCP_DEFAULT_INSTANCE
fi
echo "=== Unity container status ==="
docker inspect -f '{{.State.Status}} {{.State.Running}} {{.State.ExitCode}}' unity-mcp || true
echo "=== Unity container logs (tail 200) ==="
docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/ig' || true
echo "=== Status directory ==="
ls -la "$GITHUB_WORKSPACE/.unity-mcp" || true
jq -r . "$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json | sed -n '1,120p' || true
echo "=== Raw TCP probe to 6400 ==="
for host in 127.0.0.1 localhost; do
if timeout 2 bash -c "exec 3<>/dev/tcp/$host/6400" 2>/dev/null; then
echo "$host:6400 - SUCCESS"
else
echo "$host:6400 - FAILED"
fi
done
echo "--- PortDiscovery debug ---"
python3 - <<'PY'
import sys
sys.path.insert(0, "Server/src")
from transport.legacy.port_discovery import PortDiscovery
print(f"status_dir: {PortDiscovery.get_registry_dir()}")
instances = PortDiscovery.discover_all_unity_instances()
print(f"discover_all_unity_instances: {[{'id': i.id, 'port': i.port} for i in instances]}")
print(f"discover_unity_port: {PortDiscovery.discover_unity_port()}")
PY
echo "--- Stdio registry debug ---"
uv run --active --directory Server python - <<'PY'
from transport.legacy.stdio_port_registry import stdio_port_registry
import json
instances = stdio_port_registry.get_instances(force_refresh=True)
print(json.dumps([{"id": i.id, "port": i.port} for i in instances]))
PY
echo "=== MCP startup debug log ==="
cat "$GITHUB_WORKSPACE/.unity-mcp/mcp-server-startup-debug.log" 2>/dev/null || echo "(no startup debug log)"
# ---------- Run suite in two passes ----------
- name: Load NL prompt
id: nl_prompt
if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true'
run: |
set -euo pipefail
cp .claude/prompts/nl-unity-suite-nl.md .claude/prompts/nl-unity-suite-nl-run.md
cat >> .claude/prompts/nl-unity-suite-nl-run.md <<'EOF'
You are running the NL pass only.
- Emit exactly NL-0, NL-1, NL-2, NL-3, NL-4.
- Write each to reports/${ID}_results.xml.
- Prefer a single MultiEdit(reports/**) batch. Do not emit any T-* tests.
- Stop after NL-4_results.xml is written.
EOF
cp .claude/prompts/nl-unity-suite-nl.md .claude/prompts/nl-unity-suite-nl-retry.md
cat >> .claude/prompts/nl-unity-suite-nl-retry.md <<'EOF'
You are retrying the NL pass only.
- Emit exactly NL-0, NL-1, NL-2, NL-3, NL-4.
- Overwrite reports/${ID}_results.xml for each ID.
- Do not emit any T-* or GO-* tests.
- Stop after NL-4_results.xml is written.
EOF
{
echo "run_prompt<<__NL_RUN_PROMPT__"
cat .claude/prompts/nl-unity-suite-nl-run.md
echo "__NL_RUN_PROMPT__"
echo "retry_prompt<<__NL_RETRY_PROMPT__"
cat .claude/prompts/nl-unity-suite-nl-retry.md
echo "__NL_RETRY_PROMPT__"
} >> "$GITHUB_OUTPUT"
- name: Run Claude NL pass
uses: anthropics/claude-code-action@cc5ef44546fda0649ddde3c5ab0cd3db7b7c5035
if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true'
continue-on-error: true
env:
UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
GITHUB_STEP_SUMMARY: /dev/null
with:
prompt: ${{ steps.nl_prompt.outputs.run_prompt }}
settings: .claude/settings.json
claude_args: |
--mcp-config .claude/mcp.json
--allowedTools mcp__unity,Edit(reports/**),MultiEdit(reports/**)
--disallowedTools Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead
--model claude-haiku-4-5-20251001
--fallback-model claude-sonnet-4-5-20250929
track_progress: false
show_full_output: true
display_report: false
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Debug MCP server startup (after NL pass)
if: always()
run: |
set -eux
echo "=== MCP Server Startup Debug Log ==="
cat "$GITHUB_WORKSPACE/.unity-mcp/mcp-server-startup-debug.log" 2>/dev/null || echo "(no debug log found - MCP server may not have started)"
echo ""
echo "=== Status dir after Claude ==="
ls -la "$GITHUB_WORKSPACE/.unity-mcp" || true
- name: Check NL coverage incomplete/failed (pre-retry)
id: nl_cov
if: always()
shell: bash
run: |
set -euo pipefail
missing=()
failed=()
for id in NL-0 NL-1 NL-2 NL-3 NL-4; do
f="reports/${id}_results.xml"
if [[ ! -s "$f" && ! -s "reports/_staging/${id}_results.xml" ]]; then
missing+=("$id")
continue
fi
if [[ ! -s "$f" && -s "reports/_staging/${id}_results.xml" ]]; then
f="reports/_staging/${id}_results.xml"
fi
if grep -Eq '<(failure|error)\b' "$f"; then
failed+=("$id")
fi
done
echo "missing=${#missing[@]}" >> "$GITHUB_OUTPUT"
echo "failed=${#failed[@]}" >> "$GITHUB_OUTPUT"
if (( ${#missing[@]} )); then
echo "missing_list=${missing[*]}" >> "$GITHUB_OUTPUT"
fi
if (( ${#failed[@]} )); then
echo "failed_list=${failed[*]}" >> "$GITHUB_OUTPUT"
fi
- name: Retry NL pass (Sonnet) if incomplete or failed
if: steps.nl_cov.outputs.missing != '0' || steps.nl_cov.outputs.failed != '0'
uses: anthropics/claude-code-action@cc5ef44546fda0649ddde3c5ab0cd3db7b7c5035
continue-on-error: true
env:
UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
GITHUB_STEP_SUMMARY: /dev/null
with:
prompt: ${{ steps.nl_prompt.outputs.retry_prompt }}
settings: .claude/settings.json
claude_args: |
--mcp-config .claude/mcp.json
--allowedTools mcp__unity,Edit(reports/**),MultiEdit(reports/**)
--disallowedTools Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead
--model claude-sonnet-4-5-20250929
--fallback-model claude-haiku-4-5-20251001
track_progress: false
show_full_output: true
display_report: false
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Load T prompt
id: t_prompt
if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true'
run: |
set -euo pipefail
cp .claude/prompts/nl-unity-suite-t.md .claude/prompts/nl-unity-suite-t-run.md
cat >> .claude/prompts/nl-unity-suite-t-run.md <<'EOF'
You are running the T pass (A–J) only.
Output requirements:
- Emit exactly 10 test fragments: T-A, T-B, T-C, T-D, T-E, T-F, T-G, T-H, T-I, T-J.
- Write each fragment to reports/${ID}_results.xml (e.g., T-A_results.xml).
- Prefer a single MultiEdit(reports/**) call that writes all ten files in one batch.
- If MultiEdit is not used, emit individual writes for any missing IDs until all ten exist.
- Do not emit any NL-* fragments.
Stop condition:
- After T-J_results.xml is written, stop.
EOF
cp .claude/prompts/nl-unity-suite-t.md .claude/prompts/nl-unity-suite-t-retry.md
cat >> .claude/prompts/nl-unity-suite-t-retry.md <<'EOF'
You are running the T pass only.
Output requirements:
- Emit exactly 10 test fragments: T-A, T-B, T-C, T-D, T-E, T-F, T-G, T-H, T-I, T-J.
- Write each fragment to reports/${ID}_results.xml (e.g., T-A_results.xml).
- Prefer a single MultiEdit(reports/**) call that writes all ten files in one batch.
- If MultiEdit is not used, emit individual writes for any missing IDs until all ten exist.
- Do not emit any NL-* fragments.
Stop condition:
- After T-J_results.xml is written, stop.
EOF
{
echo "run_prompt<<__T_RUN_PROMPT__"
cat .claude/prompts/nl-unity-suite-t-run.md
echo "__T_RUN_PROMPT__"
echo "retry_prompt<<__T_RETRY_PROMPT__"
cat .claude/prompts/nl-unity-suite-t-retry.md
echo "__T_RETRY_PROMPT__"
} >> "$GITHUB_OUTPUT"
- name: Run Claude T pass A-J
uses: anthropics/claude-code-action@cc5ef44546fda0649ddde3c5ab0cd3db7b7c5035
if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true'
continue-on-error: true
env:
UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
GITHUB_STEP_SUMMARY: /dev/null
with:
prompt: ${{ steps.t_prompt.outputs.run_prompt }}
settings: .claude/settings.json
claude_args: |
--mcp-config .claude/mcp.json
--allowedTools mcp__unity,Edit(reports/**),MultiEdit(reports/**)
--disallowedTools Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead
--model claude-haiku-4-5-20251001
--fallback-model claude-sonnet-4-5-20250929
track_progress: false
show_full_output: true
display_report: false
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# (moved) Assert T coverage after staged fragments are promoted
- name: Check T coverage incomplete (pre-retry)
id: t_cov
if: always()
shell: bash
run: |
set -euo pipefail
missing=()
for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do
if [[ ! -s "reports/${id}_results.xml" && ! -s "reports/_staging/${id}_results.xml" ]]; then
missing+=("$id")
fi
done
echo "missing=${#missing[@]}" >> "$GITHUB_OUTPUT"
if (( ${#missing[@]} )); then
echo "list=${missing[*]}" >> "$GITHUB_OUTPUT"
fi
- name: Retry T pass (Sonnet) if incomplete
if: steps.t_cov.outputs.missing != '0'
uses: anthropics/claude-code-action@cc5ef44546fda0649ddde3c5ab0cd3db7b7c5035
env:
GITHUB_STEP_SUMMARY: /dev/null
with:
prompt: ${{ steps.t_prompt.outputs.retry_prompt }}
settings: .claude/settings.json
claude_args: |
--mcp-config .claude/mcp.json
--allowedTools mcp__unity,Edit(reports/**),MultiEdit(reports/**)
--disallowedTools Bash,MultiEdit(/!(reports/**)),WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead
--model claude-sonnet-4-5-20250929
--fallback-model claude-haiku-4-5-20251001
track_progress: false
show_full_output: true
display_report: false
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Re-assert T coverage (post-retry)
if: always()
shell: bash
run: |
set -euo pipefail
missing=()
for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do
[[ -s "reports/${id}_results.xml" ]] || missing+=("$id")
done
if (( ${#missing[@]} )); then
echo "::error::Still missing T fragments: ${missing[*]}"
exit 1
fi
# ---------- Run GO pass (GameObject API tests) ----------
- name: Load GO prompt
id: go_prompt
if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true'
run: |
set -euo pipefail
cp .claude/prompts/nl-gameobject-suite.md .claude/prompts/nl-gameobject-suite-run.md
cat >> .claude/prompts/nl-gameobject-suite-run.md <<'EOF'
You are running the GO pass (GameObject API tests) only.
Output requirements:
- Emit exactly 11 test fragments: GO-0, GO-1, GO-2, GO-3, GO-4, GO-5, GO-6, GO-7, GO-8, GO-9, GO-10.
- Write each fragment to reports/${ID}_results.xml (e.g., GO-0_results.xml).
- Prefer a single MultiEdit(reports/**) call that writes all eleven files in one batch.
- Do not emit any NL-* or T-* fragments.
Stop condition:
- After GO-10_results.xml is written, stop.
EOF
cp .claude/prompts/nl-gameobject-suite.md .claude/prompts/nl-gameobject-suite-retry.md
cat >> .claude/prompts/nl-gameobject-suite-retry.md <<'EOF'
You are running the GO pass only.
Output requirements:
- Emit exactly 11 test fragments: GO-0, GO-1, GO-2, GO-3, GO-4, GO-5, GO-6, GO-7, GO-8, GO-9, GO-10.
- Write each fragment to reports/${ID}_results.xml (e.g., GO-0_results.xml).
- Prefer a single MultiEdit(reports/**) call that writes all eleven files in one batch.
- Do not emit any NL-* or T-* fragments.
Stop condition:
- After GO-10_results.xml is written, stop.
EOF
{
echo "run_prompt<<__GO_RUN_PROMPT__"
cat .claude/prompts/nl-gameobject-suite-run.md
echo "__GO_RUN_PROMPT__"
echo "retry_prompt<<__GO_RETRY_PROMPT__"
cat .claude/prompts/nl-gameobject-suite-retry.md
echo "__GO_RETRY_PROMPT__"
} >> "$GITHUB_OUTPUT"
- name: Run Claude GO pass
uses: anthropics/claude-code-action@cc5ef44546fda0649ddde3c5ab0cd3db7b7c5035
if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true'
continue-on-error: true
env:
UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
GITHUB_STEP_SUMMARY: /dev/null
with:
prompt: ${{ steps.go_prompt.outputs.run_prompt }}
settings: .claude/settings.json
claude_args: |
--mcp-config .claude/mcp.json
--allowedTools mcp__unity,Edit(reports/**),MultiEdit(reports/**)
--disallowedTools Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead
--model claude-haiku-4-5-20251001
--fallback-model claude-sonnet-4-5-20250929
track_progress: false
show_full_output: true
display_report: false
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Check GO coverage incomplete (pre-retry)
id: go_cov
if: always()
shell: bash
run: |
set -euo pipefail
missing=()
for id in GO-0 GO-1 GO-2 GO-3 GO-4 GO-5 GO-6 GO-7 GO-8 GO-9 GO-10; do
if [[ ! -s "reports/${id}_results.xml" && ! -s "reports/_staging/${id}_results.xml" ]]; then
missing+=("$id")
fi
done
echo "missing=${#missing[@]}" >> "$GITHUB_OUTPUT"
if (( ${#missing[@]} )); then
echo "list=${missing[*]}" >> "$GITHUB_OUTPUT"
fi
- name: Retry GO pass (Sonnet) if incomplete
if: steps.go_cov.outputs.missing != '0'
uses: anthropics/claude-code-action@cc5ef44546fda0649ddde3c5ab0cd3db7b7c5035
env:
GITHUB_STEP_SUMMARY: /dev/null
with:
prompt: ${{ steps.go_prompt.outputs.retry_prompt }}
settings: .claude/settings.json
claude_args: |
--mcp-config .claude/mcp.json
--allowedTools mcp__unity,Edit(reports/**),MultiEdit(reports/**)
--disallowedTools Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead
--model claude-sonnet-4-5-20250929
--fallback-model claude-haiku-4-5-20251001
track_progress: false
show_full_output: true
display_report: false
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# (kept) Finalize staged report fragments (promote to reports/)
# (removed duplicate) Finalize staged report fragments
- name: Assert T coverage (after promotion)
if: always()
shell: bash
run: |
set -euo pipefail
missing=()
for id in T-A T-B T-C T-D T-E T-F T-G T-H T-I T-J; do
if [[ ! -s "reports/${id}_results.xml" ]]; then
# Accept staged fragment as present
[[ -s "reports/_staging/${id}_results.xml" ]] || missing+=("$id")
fi
done
if (( ${#missing[@]} )); then
echo "::error::Missing T fragments: ${missing[*]}"
exit 1
fi
- name: Canonicalize testcase names (NL/T prefixes)
if: always()
shell: bash
run: |
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET, re, os
RULES = [
("NL-0", r"\b(NL-0|Baseline|State\s*Capture)\b"),
("NL-1", r"\b(NL-1|Core\s*Method)\b"),
("NL-2", r"\b(NL-2|Anchor|Build\s*marker)\b"),
("NL-3", r"\b(NL-3|End[-\s]*of[-\s]*Class\s*Content|Tail\s*test\s*[ABC])\b"),
("NL-4", r"\b(NL-4|Console|Unity\s*console)\b"),
("T-A", r"\b(T-?A|Temporary\s*Helper)\b"),
("T-B", r"\b(T-?B|Method\s*Body\s*Interior)\b"),
("T-C", r"\b(T-?C|Different\s*Method\s*Interior|ApplyBlend)\b"),
("T-D", r"\b(T-?D|End[-\s]*of[-\s]*Class\s*Helper|TestHelper)\b"),
("T-E", r"\b(T-?E|Method\s*Evolution|Counter|IncrementCounter)\b"),
("T-F", r"\b(T-?F|Atomic\s*Multi[-\s]*Edit)\b"),
("T-G", r"\b(T-?G|Path\s*Normalization)\b"),
("T-H", r"\b(T-?H|Validation\s*on\s*Modified)\b"),
("T-I", r"\b(T-?I|Failure\s*Surface)\b"),
("T-J", r"\b(T-?J|Idempotenc(y|e))\b"),
("GO-0", r"\b(GO-?0|Hierarchy.*ComponentTypes)\b"),
("GO-1", r"\b(GO-?1|Find\s*GameObjects\s*Tool)\b"),
("GO-2", r"\b(GO-?2|GameObject\s*Resource)\b"),
("GO-3", r"\b(GO-?3|Components\s*Resource)\b"),
("GO-4", r"\b(GO-?4|Manage\s*Components)\b"),
("GO-5", r"\b(GO-?5|Find.*by.*Name)\b"),
("GO-6", r"\b(GO-?6|Find.*by.*Tag)\b"),
("GO-7", r"\b(GO-?7|Single\s*Component)\b"),
("GO-8", r"\b(GO-?8|Remove\s*Component)\b"),
("GO-9", r"\b(GO-?9|Pagination)\b"),
("GO-10", r"\b(GO-?10|Deprecation)\b"),
]
def canon_name(name: str) -> str:
n = name or ""
for tid, pat in RULES:
if re.search(pat, n, flags=re.I):
# If it already starts with the correct format, leave it alone
if re.match(rf'^\s*{re.escape(tid)}\s*[—–-]', n, flags=re.I):
return n.strip()
# If it has a different separator, extract title and reformat
title_match = re.search(rf'{re.escape(tid)}\s*[:.\-–—]\s*(.+)', n, flags=re.I)
if title_match:
title = title_match.group(1).strip()
return f"{tid} — {title}"
# Otherwise, just return the canonical ID
return tid
return n
def id_from_filename(p: Path):
n = p.name
m = re.match(r'NL-?(\d+)_results\.xml$', n, re.I)
if m:
return f"NL-{int(m.group(1))}"
m = re.match(r'T-?([A-J])_results\.xml$', n, re.I)
if m:
return f"T-{m.group(1).upper()}"
m = re.match(r'GO-?(\d+)_results\.xml$', n, re.I)
if m:
return f"GO-{int(m.group(1))}"
return None
frags = list(sorted(Path("reports").glob("*_results.xml")))
for frag in frags:
try:
tree = ET.parse(frag); root = tree.getroot()
except Exception:
continue
if root.tag != "testcase":
continue
file_id = id_from_filename(frag)
old = root.get("name") or ""
# Prefer filename-derived ID; if name doesn't start with it, override
if file_id:
# Respect file's ID (prevents T-D being renamed to NL-3 by loose patterns)
title = re.sub(r'^\s*(NL-\d+|T-[A-Z]|GO-\d+)\s*[—–:\-]\s*', '', old).strip()
new = f"{file_id} — {title}" if title else file_id
else:
new = canon_name(old)
if new != old and new:
root.set("name", new)
tree.write(frag, encoding="utf-8", xml_declaration=False)
print(f'canon: {frag.name}: "{old}" -> "{new}"')
# Note: Do not auto-relable fragments. We rely on per-test strict emission
# and the backfill step to surface missing tests explicitly.
PY
- name: Backfill missing NL/T tests (fail placeholders)
if: always()
shell: bash
run: |
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET
import re
import shutil
DESIRED = ["NL-0","NL-1","NL-2","NL-3","NL-4","T-A","T-B","T-C","T-D","T-E","T-F","T-G","T-H","T-I","T-J","GO-0","GO-1","GO-2","GO-3","GO-4","GO-5","GO-6","GO-7","GO-8","GO-9","GO-10"]
seen = set()
bad = set()
def id_from_filename(p: Path):
n = p.name
m = re.match(r'NL-?(\d+)_results\.xml$', n, re.I)
if m:
return f"NL-{int(m.group(1))}"
m = re.match(r'T-?([A-J])_results\.xml$', n, re.I)
if m:
return f"T-{m.group(1).upper()}"
m = re.match(r'GO-?(\d+)_results\.xml$', n, re.I)
if m:
return f"GO-{int(m.group(1))}"
return None
for p in Path("reports").glob("*_results.xml"):
fid = id_from_filename(p)
try:
r = ET.parse(p).getroot()
except Exception:
# If the file exists but isn't parseable, preserve it for debugging and
# treat it as a failing (malformed) fragment rather than "not produced".
if fid in DESIRED and p.exists() and p.stat().st_size > 0:
staging = Path("reports/_staging")
staging.mkdir(parents=True, exist_ok=True)
preserved = staging / f"{fid}_malformed.xml"
try:
shutil.copyfile(p, preserved)
except Exception:
pass
bad.add(fid)
continue
# Count by filename id primarily; fall back to testcase name if needed
if fid in DESIRED:
seen.add(fid)
continue
if r.tag == "testcase":
name = (r.get("name") or "").strip()
for d in DESIRED:
if name.startswith(d):
seen.add(d)
break
Path("reports").mkdir(parents=True, exist_ok=True)
for d in DESIRED:
if d in seen:
continue
frag = Path(f"reports/{d}_results.xml")
tc = ET.Element("testcase", {"classname":"UnityMCP.NL-T", "name": d})
if d in bad:
fail = ET.SubElement(tc, "failure", {"message":"malformed xml"})
fail.text = "The agent wrote a fragment file, but it was not valid XML (parse failed). See reports/_staging/*_malformed.xml for the preserved original."
else:
fail = ET.SubElement(tc, "failure", {"message":"not produced"})
fail.text = "The agent did not emit a fragment for this test."
ET.ElementTree(tc).write(frag, encoding="utf-8", xml_declaration=False)
print(f"backfill: {d}")
PY
- name: "Debug: list testcase names"
if: always()
run: |
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET
for p in sorted(Path('reports').glob('*_results.xml')):
try:
r = ET.parse(p).getroot()
if r.tag == 'testcase':
print(f"{p.name}: {(r.get('name') or '').strip()}")
except Exception:
pass
PY
# ---------- Merge testcase fragments into JUnit ----------
- name: Normalize/assemble JUnit in-place (single file)
if: always()
shell: bash
run: |
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET
import re, os
def localname(tag: str) -> str:
return tag.rsplit('}', 1)[-1] if '}' in tag else tag
src = Path(os.environ.get('JUNIT_OUT', 'reports/junit-nl-suite.xml'))
if not src.exists():
raise SystemExit(0)
tree = ET.parse(src)
root = tree.getroot()
suite = root.find('./*') if localname(root.tag) == 'testsuites' else root
if suite is None:
raise SystemExit(0)
def id_from_filename(p: Path):
n = p.name
m = re.match(r'NL-?(\d+)_results\.xml$', n, re.I)
if m:
return f"NL-{int(m.group(1))}"
m = re.match(r'T-?([A-J])_results\.xml$', n, re.I)
if m:
return f"T-{m.group(1).upper()}"
m = re.match(r'GO-?(\d+)_results\.xml$', n, re.I)
if m:
return f"GO-{int(m.group(1))}"
return None
def id_from_system_out(tc):
so = tc.find('system-out')
if so is not None and so.text:
m = re.search(r'\b(NL-\d+|T-[A-Z]|GO-\d+)\b', so.text)
if m:
return m.group(1)
return None
fragments = sorted(Path('reports').glob('*_results.xml'))
report_names = {p.name for p in fragments}
fragments += sorted(p for p in Path('reports/_staging').glob('*_results.xml') if p.name not in report_names)
if fragments:
print("merge fragments:", ", ".join(p.as_posix() for p in fragments))
added = 0
renamed = 0
for frag in fragments:
tcs = []
try:
froot = ET.parse(frag).getroot()
if localname(froot.tag) == 'testcase':
tcs = [froot]
else:
tcs = list(froot.findall('.//testcase'))
except Exception:
txt = Path(frag).read_text(encoding='utf-8', errors='replace')
# Extract all testcase nodes from raw text
nodes = re.findall(r'', txt, flags=re.DOTALL)
for m in nodes:
try:
tcs.append(ET.fromstring(m))
except Exception:
pass
# Guard: keep only the first testcase from each fragment
if len(tcs) > 1:
tcs = tcs[:1]
test_id = id_from_filename(frag)
for tc in tcs:
current_name = tc.get('name') or ''
tid = test_id or id_from_system_out(tc)
# Enforce filename-derived ID as prefix; repair names if needed
if tid and not re.match(r'^\s*(NL-\d+|T-[A-Z]|GO-\d+)\b', current_name):
title = current_name.strip()
new_name = f'{tid} — {title}' if title else tid
tc.set('name', new_name)
elif tid and not re.match(rf'^\s*{re.escape(tid)}\b', current_name):
# Replace any wrong leading ID with the correct one
title = re.sub(r'^\s*(NL-\d+|T-[A-Z]|GO-\d+)\s*[—–:\-]\s*', '', current_name).strip()
new_name = f'{tid} — {title}' if title else tid
tc.set('name', new_name)
renamed += 1
suite.append(tc)
added += 1
print(f"merge add: {frag.name} -> {tc.get('name')}")
if added:
# Drop bootstrap placeholder and recompute counts
for tc in list(suite.findall('.//testcase')):
if (tc.get('name') or '') == 'NL-Suite.Bootstrap':
suite.remove(tc)
testcases = suite.findall('.//testcase')
failures_cnt = sum(1 for tc in testcases if (tc.find('failure') is not None or tc.find('error') is not None))
suite.set('tests', str(len(testcases)))
suite.set('failures', str(failures_cnt))
suite.set('errors', '0')
suite.set('skipped', '0')
tree.write(src, encoding='utf-8', xml_declaration=True)
print(f"Appended {added} testcase(s); renamed {renamed} to canonical NL/T names.")
PY
# Guard is GO-specific; only parse GO fragments here.
- name: "Guard: ensure GO fragments merged into JUnit"
if: always()
shell: bash
run: |
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET
import os, re
def localname(tag: str) -> str:
return tag.rsplit('}', 1)[-1] if '}' in tag else tag
junit_path = Path(os.environ.get('JUNIT_OUT', 'reports/junit-nl-suite.xml'))
if not junit_path.exists():
raise SystemExit(0)
tree = ET.parse(junit_path)
root = tree.getroot()
suite = root.find('./*') if localname(root.tag) == 'testsuites' else root
if suite is None:
raise SystemExit(0)
def id_from_filename(p: Path):
n = p.name
m = re.match(r'GO-?(\d+)_results\.xml$', n, re.I)
if m:
return f"GO-{int(m.group(1))}"
return None
expected = set()
for p in list(Path("reports").glob("GO-*_results.xml")) + list(Path("reports/_staging").glob("GO-*_results.xml")):
fid = id_from_filename(p)
if fid:
expected.add(fid)
seen = set()
for tc in suite.findall('.//testcase'):
name = (tc.get('name') or '').strip()
m = re.match(r'(GO-\d+)\b', name)
if m:
seen.add(m.group(1))
missing = sorted(expected - seen)
if missing:
print(f"::error::GO fragments present but not merged into JUnit: {' '.join(missing)}")
raise SystemExit(1)
PY
# ---------- Markdown summary from JUnit ----------
- name: Build markdown summary from JUnit
if: always()
shell: bash
run: |
python3 - <<'PY'
import xml.etree.ElementTree as ET
from pathlib import Path
import os, html, re
def localname(tag: str) -> str:
return tag.rsplit('}', 1)[-1] if '}' in tag else tag
src = Path(os.environ.get('JUNIT_OUT', 'reports/junit-nl-suite.xml'))
md_out = Path(os.environ.get('MD_OUT', 'reports/junit-nl-suite.md'))
md_out.parent.mkdir(parents=True, exist_ok=True)
if not src.exists():
md_out.write_text("# Unity NL/T Editing Suite Test Results\n\n(No JUnit found)\n", encoding='utf-8')
raise SystemExit(0)
tree = ET.parse(src)
root = tree.getroot()
suite = root.find('./*') if localname(root.tag) == 'testsuites' else root
cases = [] if suite is None else list(suite.findall('.//testcase'))
def id_from_case(tc):
n = (tc.get('name') or '')
m = re.match(r'\s*(NL-\d+|T-[A-Z]|GO-\d+)\b', n)
if m:
return m.group(1)
so = tc.find('system-out')
if so is not None and so.text:
m = re.search(r'\b(NL-\d+|T-[A-Z]|GO-\d+)\b', so.text)
if m:
return m.group(1)
return None
id_status = {}
name_map = {}
for tc in cases:
tid = id_from_case(tc)
ok = (tc.find('failure') is None and tc.find('error') is None)
if tid and tid not in id_status:
id_status[tid] = ok
name_map[tid] = (tc.get('name') or tid)
desired = ['NL-0','NL-1','NL-2','NL-3','NL-4','T-A','T-B','T-C','T-D','T-E','T-F','T-G','T-H','T-I','T-J','GO-0','GO-1','GO-2','GO-3','GO-4','GO-5','GO-6','GO-7','GO-8','GO-9','GO-10']
default_titles = {
'NL-0': 'Baseline State Capture',
'NL-1': 'Core Method Operations',
'NL-2': 'Anchor Comment Insertion',
'NL-3': 'End-of-Class Content',
'NL-4': 'Console State Verification',
'T-A': 'Temporary Helper',
'T-B': 'Method Body Interior',
'T-C': 'Different Method Interior',
'T-D': 'End-of-Class Helper',
'T-E': 'Method Evolution',
'T-F': 'Atomic Multi-Edit',
'T-G': 'Path Normalization',
'T-H': 'Validation on Modified',
'T-I': 'Failure Surface',
'T-J': 'Idempotency',
'GO-0': 'Hierarchy with ComponentTypes',
'GO-1': 'Find GameObjects Tool',
'GO-2': 'GameObject Resource Read',
'GO-3': 'Components Resource Read',
'GO-4': 'Manage Components Tool',
'GO-5': 'Find GameObjects by Name',
'GO-6': 'Find GameObjects by Tag',
'GO-7': 'Single Component Resource Read',
'GO-8': 'Remove Component',
'GO-9': 'Find with Pagination',
'GO-10': 'Deprecation Warnings',
}
def display_name(test_id: str) -> str:
# Prefer the emitted testcase "name" attribute (it may already include ID + title).
n = (name_map.get(test_id) or '').strip()
if n:
return n
t = (default_titles.get(test_id) or '').strip()
return f"{test_id} — {t}" if t else test_id
total = len(cases)
failures = sum(1 for tc in cases if (tc.find('failure') is not None or tc.find('error') is not None))
passed = total - failures
lines = []
lines += [
'# Unity NL/T Editing Suite Test Results',
'',
f'Totals: {passed} passed, {failures} failed, {total} total',
'',
'## Test Checklist'
]
for p in desired:
st = id_status.get(p, None)
label = display_name(p)
lines.append(f"- [x] {label}" if st is True else (f"- [ ] {label} (fail)" if st is False else f"- [ ] {label} (not run)"))
lines.append('')
lines.append('## Test Details (trimmed)')
def order_key(n: str):
if n.startswith('NL-'):
try:
return (0, int(n.split('-')[1]))
except:
return (0, 999)
if n.startswith('T-') and len(n) > 2:
return (1, ord(n[2]))
if n.startswith('GO-'):
try:
return (2, int(n.split('-')[1]))
except:
return (2, 999)
return (3, n)
MAX_CHARS = 800
MAX_LINES = 8
seen = set()
for tid in sorted(id_status.keys(), key=order_key):
seen.add(tid)
tc = next((c for c in cases if (id_from_case(c) == tid)), None)
if not tc:
continue
title = name_map.get(tid, tid)
status_badge = "PASS" if id_status[tid] else "FAIL"
lines.append(f"### {title} — {status_badge}")
so = tc.find('system-out')
text = '' if so is None or so.text is None else html.unescape(so.text.replace('\r\n','\n'))
if text.strip():
t = text.strip()
truncated = False
lines_out = t.splitlines()
if len(lines_out) > MAX_LINES:
t = "\n".join(lines_out[:MAX_LINES]).rstrip()
truncated = True
if len(t) > MAX_CHARS:
t = t[:MAX_CHARS].rstrip()
truncated = True
if truncated:
t += "\n…(truncated)"
fence = '```' if '```' not in t else '````'
lines += [fence, t, fence]
else:
lines.append('(no system-out)')
node = tc.find('failure') or tc.find('error')
if node is not None:
msg = (node.get('message') or '').strip()
body = (node.text or '').strip()
if msg:
lines.append(f"- Message: {msg}")
if body:
lines.append(f"- Detail: {body.splitlines()[0][:500]}")
lines.append('')
for tc in cases:
if id_from_case(tc) in seen:
continue
title = tc.get('name') or '(unnamed)'
status_badge = "PASS" if (tc.find('failure') is None and tc.find('error') is None) else "FAIL"
lines.append(f"### {title} — {status_badge}")
lines.append('(unmapped test id)')
lines.append('')
md_out.write_text('\n'.join(lines), encoding='utf-8')
PY
# ---------- CI gate: fail job if any NL/T test missing or failed ----------
- name: Fail CI if NL/T incomplete or failed
if: always()
shell: bash
run: |
python3 - <<'PY'
import os, re, sys
from pathlib import Path
import xml.etree.ElementTree as ET
desired = ['NL-0','NL-1','NL-2','NL-3','NL-4','T-A','T-B','T-C','T-D','T-E','T-F','T-G','T-H','T-I','T-J','GO-0','GO-1','GO-2','GO-3','GO-4','GO-5','GO-6','GO-7','GO-8','GO-9','GO-10']
junit_path = Path(os.environ.get('JUNIT_OUT', 'reports/junit-nl-suite.xml'))
if not junit_path.exists():
print("::error::No JUnit output found; failing CI gate.")
sys.exit(1)
def localname(tag: str) -> str:
return tag.rsplit('}', 1)[-1] if '}' in tag else tag
tree = ET.parse(junit_path)
root = tree.getroot()
suite = root.find('./*') if localname(root.tag) == 'testsuites' else root
cases = [] if suite is None else list(suite.findall('.//testcase'))
def id_from_case(tc):
name = (tc.get('name') or '').strip()
m = re.match(r'(NL-\d+|T-[A-Z]|GO-\d+)\b', name)
if m:
return m.group(1)
so = tc.find('system-out')
if so is not None and so.text:
m = re.search(r'\b(NL-\d+|T-[A-Z]|GO-\d+)\b', so.text)
if m:
return m.group(1)
return None
# Determine status per desired ID (first occurrence wins, matching the summary builder)
id_status = {}
for tc in cases:
tid = id_from_case(tc)
if not tid or tid not in desired or tid in id_status:
continue
ok = (tc.find('failure') is None and tc.find('error') is None)
id_status[tid] = ok
missing = [d for d in desired if d not in id_status]
failed = [d for d, ok in id_status.items() if ok is False]
if missing:
print(f"::error::Missing NL/T tests in JUnit: {' '.join(missing)}")
if failed:
print(f"::error::Failing NL/T tests in JUnit: {' '.join(sorted(failed))}")
# Gate: all desired must be present and passing
if missing or failed:
sys.exit(1)
print("NL/T CI gate passed: all required tests present and passing.")
PY
# ---------- Collect execution transcript (if present) ----------
- name: Collect action execution transcript
if: always()
shell: bash
run: |
set -eux
if [ -f "$RUNNER_TEMP/claude-execution-output.json" ]; then
cp "$RUNNER_TEMP/claude-execution-output.json" reports/claude-execution-output.json
elif [ -f "/home/runner/work/_temp/claude-execution-output.json" ]; then
cp "/home/runner/work/_temp/claude-execution-output.json" reports/claude-execution-output.json
fi
- name: Sanitize markdown (normalize newlines)
if: always()
run: |
set -eu
python3 - <<'PY'
from pathlib import Path
rp=Path('reports'); rp.mkdir(parents=True, exist_ok=True)
for p in rp.glob('*.md'):
b=p.read_bytes().replace(b'\x00', b'')
s=b.decode('utf-8','replace').replace('\r\n','\n')
p.write_text(s, encoding='utf-8', newline='\n')
PY
- name: NL/T details -> Job Summary
if: always()
run: |
python3 - <<'PY' >> $GITHUB_STEP_SUMMARY
from pathlib import Path
print("## Unity NL/T Editing Suite — Summary")
print("")
p = Path('reports/junit-nl-suite.md')
if not p.exists():
print("_No markdown report found._")
raise SystemExit(0)
text = p.read_bytes().decode('utf-8', 'replace').replace('\r\n', '\n')
lines = text.splitlines()
details_start = None
for i, line in enumerate(lines):
if line.startswith("## Test Details"):
details_start = i
break
# Keep the summary compact: show heading/totals/checklist only.
prefix_lines = lines if details_start is None else lines[:details_start]
prefix = "\n".join(prefix_lines).strip()
if prefix:
print(prefix)
else:
print("_No summary section found in markdown report._")
failed_blocks = []
if details_start is not None:
i = details_start + 1
while i < len(lines):
line = lines[i]
if line.startswith("### "):
block = [line]
i += 1
while i < len(lines) and not lines[i].startswith("### "):
block.append(lines[i])
i += 1
if " — FAIL" in line or " - FAIL" in line:
failed_blocks.append(block)
continue
i += 1
if failed_blocks:
print("")
print("## Failing Test Details")
print("")
max_block_lines = 40
for block in failed_blocks:
if len(block) > max_block_lines:
block = block[:max_block_lines] + ["…(truncated)"]
print("\n".join(block).rstrip())
print("")
else:
print("")
print("_All tests passed. Full per-test details are in artifact file `reports/junit-nl-suite.md`._")
PY
- name: Fallback JUnit if missing
if: always()
run: |
set -eu
mkdir -p reports
if [ ! -f "$JUNIT_OUT" ]; then
printf '%s\n' \
'' \
'' \
' ' \
' ' \
' ' \
'' \
> "$JUNIT_OUT"
fi
- name: Publish JUnit report
if: always()
uses: mikepenz/action-junit-report@v5
with:
report_paths: "${{ env.JUNIT_OUT }}"
include_passed: false
detailed_summary: false
annotate_notice: false
check_annotations: false
job_summary: false
verbose_summary: false
skip_success_summary: true
require_tests: false
fail_on_parse_error: true
- name: Upload artifacts (reports + fragments + transcript + debug)
if: always()
uses: actions/upload-artifact@v4
with:
name: claude-nl-suite-artifacts
path: |
${{ env.JUNIT_OUT }}
${{ env.MD_OUT }}
reports/*_results.xml
reports/claude-execution-output.json
${{ github.workspace }}/.unity-mcp/mcp-server-startup-debug.log
retention-days: 7
# ---------- Always stop Unity ----------
- name: Stop Unity
if: always()
run: |
docker logs --tail 400 unity-mcp | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/ig' || true
docker rm -f unity-mcp || true
- name: Return Pro license (if used)
if: always() && steps.lic.outputs.use_ebl == 'true' && steps.lic.outputs.has_serial == 'true'
uses: game-ci/unity-return-license@v2
continue-on-error: true
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
================================================
FILE: .github/workflows/github-repo-stats.yml
================================================
name: github-repo-stats
on:
# schedule:
# Run this once per day, towards the end of the day for keeping the most
# recent data point most meaningful (hours are interpreted in UTC).
#- cron: "0 23 * * *"
workflow_dispatch: # Allow for running this manually.
jobs:
j1:
if: github.repository == 'CoplayDev/unity-mcp'
name: github-repo-stats
runs-on: ubuntu-latest
steps:
- name: run-ghrs
# Use latest release.
uses: jgehrcke/github-repo-stats@RELEASE
with:
ghtoken: ${{ secrets.ghrs_github_api_token }}
================================================
FILE: .github/workflows/python-tests.yml
================================================
name: Python Tests
on:
push:
branches: ["**"]
paths:
- Server/**
- .github/workflows/python-tests.yml
workflow_dispatch: {}
jobs:
test:
name: Run Python Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.10
- name: Install dependencies
run: |
cd Server
uv sync
uv pip install -e ".[dev]"
- name: Run tests with coverage
run: |
cd Server
uv run pytest tests/ -v --tb=short --cov --cov-report=xml --cov-report=html --cov-report=term
- name: Upload coverage reports
uses: codecov/codecov-action@v4
if: always()
with:
files: ./Server/coverage.xml
flags: python
name: python-coverage
fail_ci_if_error: false
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: pytest-results
path: |
Server/.pytest_cache/
Server/tests/
Server/coverage.xml
Server/htmlcov/
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
concurrency:
group: release-main
cancel-in-progress: false
on:
workflow_dispatch:
inputs:
version_bump:
description: "Version bump type (none = release beta version as-is)"
type: choice
options:
- patch
- minor
- major
- none
default: patch
required: true
jobs:
bump:
name: Bump version, tag, and create release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
new_version: ${{ steps.compute.outputs.new_version }}
tag: ${{ steps.tag.outputs.tag }}
bump_branch: ${{ steps.bump_branch.outputs.name }}
steps:
- name: Ensure workflow is running on main
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_REF_NAME}" != "main" ]]; then
echo "This workflow must be run on the main branch. Current ref: ${GITHUB_REF_NAME}" >&2
exit 1
fi
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
- name: Show current versions
id: preview
shell: bash
run: |
set -euo pipefail
echo "============================================"
echo "CURRENT VERSION STATUS"
echo "============================================"
# Get main version
MAIN_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
MAIN_PYPI=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
echo "Main branch:"
echo " Unity package: $MAIN_VERSION"
echo " PyPI server: $MAIN_PYPI"
echo ""
# Get beta version
git fetch origin beta
BETA_VERSION=$(git show origin/beta:MCPForUnity/package.json | jq -r '.version')
BETA_PYPI=$(git show origin/beta:Server/pyproject.toml | grep -oP '(?<=version = ")[^"]+')
echo "Beta branch:"
echo " Unity package: $BETA_VERSION"
echo " PyPI server: $BETA_PYPI"
echo ""
# Compute stripped version (used for "none" bump option)
STRIPPED=$(echo "$BETA_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//')
echo "stripped_version=$STRIPPED" >> "$GITHUB_OUTPUT"
# Show what will happen
BUMP="${{ inputs.version_bump }}"
echo "Selected bump type: $BUMP"
echo "After stripping beta suffix: $STRIPPED"
if [[ "$BUMP" == "none" ]]; then
echo "Release version will be: $STRIPPED"
else
IFS='.' read -r MA MI PA <<< "$STRIPPED"
case "$BUMP" in
major) ((MA+=1)); MI=0; PA=0 ;;
minor) ((MI+=1)); PA=0 ;;
patch) ((PA+=1)) ;;
esac
echo "Release version will be: $MA.$MI.$PA"
fi
echo "============================================"
- name: Merge beta into main
shell: bash
run: |
set -euo pipefail
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
# Fetch beta branch
git fetch origin beta
# Check if beta has changes not in main
if git merge-base --is-ancestor origin/beta HEAD; then
echo "beta is already merged into main. Nothing to merge."
else
echo "Merging beta into main..."
git merge origin/beta --no-edit -m "chore: merge beta into main for release"
echo "Beta merged successfully."
fi
- name: Strip beta suffix from version if present
shell: bash
run: |
set -euo pipefail
CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
echo "Current version: $CURRENT_VERSION"
# Strip beta/alpha/rc suffix if present (e.g., "9.4.0-beta.1" -> "9.4.0")
if [[ "$CURRENT_VERSION" == *"-"* ]]; then
STABLE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//')
# Validate we have a proper X.Y.Z format after stripping
if ! [[ "$STABLE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Could not parse version '$CURRENT_VERSION' -> '$STABLE_VERSION'" >&2
exit 1
fi
echo "Stripping prerelease suffix: $CURRENT_VERSION -> $STABLE_VERSION"
jq --arg v "$STABLE_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
mv tmp.json MCPForUnity/package.json
# Also update pyproject.toml
sed -i "s/^version = .*/version = \"${STABLE_VERSION}\"/" Server/pyproject.toml
else
echo "Version is already stable: $CURRENT_VERSION"
fi
- name: Compute new version
id: compute
env:
PREVIEWED_STRIPPED: ${{ steps.preview.outputs.stripped_version }}
shell: bash
run: |
set -euo pipefail
BUMP="${{ inputs.version_bump }}"
CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
echo "Current version: $CURRENT_VERSION"
# Sanity check: ensure current version matches what was previewed
if [[ "$CURRENT_VERSION" != "$PREVIEWED_STRIPPED" ]]; then
echo "Warning: Current version ($CURRENT_VERSION) differs from previewed ($PREVIEWED_STRIPPED)"
echo "This may indicate an unexpected merge result. Proceeding with current version."
fi
if [[ "$BUMP" == "none" ]]; then
# Use the previewed stripped version to ensure consistency with what user saw
NEW_VERSION="$PREVIEWED_STRIPPED"
else
IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION"
case "$BUMP" in
major)
((MA+=1)); MI=0; PA=0
;;
minor)
((MI+=1)); PA=0
;;
patch)
((PA+=1))
;;
*)
echo "Unknown version_bump: $BUMP" >&2
exit 1
;;
esac
NEW_VERSION="$MA.$MI.$PA"
fi
echo "New version: $NEW_VERSION"
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
- name: Compute tag
id: tag
env:
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT"
- name: Update files to new version
env:
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
echo "Updating all version references to $NEW_VERSION"
python3 tools/update_versions.py --version "$NEW_VERSION"
- name: Commit version bump to a temporary branch
id: bump_branch
env:
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
BRANCH="release/v${NEW_VERSION}"
echo "name=$BRANCH" >> "$GITHUB_OUTPUT"
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout -b "$BRANCH"
git add MCPForUnity/package.json manifest.json "Server/pyproject.toml" Server/README.md
if git diff --cached --quiet; then
echo "No version changes to commit."
else
git commit -m "chore: bump version to ${NEW_VERSION}"
fi
echo "Pushing bump branch $BRANCH"
git push origin "$BRANCH"
- name: Create PR for version bump into main
id: bump_pr
env:
GH_TOKEN: ${{ github.token }}
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
BRANCH: ${{ steps.bump_branch.outputs.name }}
shell: bash
run: |
set -euo pipefail
PR_URL=$(gh pr create \
--base main \
--head "$BRANCH" \
--title "chore: bump version to ${NEW_VERSION}" \
--body "Automated version bump to ${NEW_VERSION}.")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Enable auto-merge and merge PR
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.bump_pr.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
# Enable auto-merge (requires repo setting "Allow auto-merge")
gh pr merge "$PR_NUMBER" --merge --auto || true
# Wait for PR to be merged (poll up to 2 minutes)
for i in {1..24}; do
STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state')
if [[ "$STATE" == "MERGED" ]]; then
echo "PR merged successfully."
exit 0
fi
echo "Waiting for PR to merge... (state: $STATE)"
sleep 5
done
echo "PR did not merge in time. Attempting direct merge..."
gh pr merge "$PR_NUMBER" --merge
- name: Fetch merged main and create tag
env:
TAG: ${{ steps.tag.outputs.tag }}
shell: bash
run: |
set -euo pipefail
git fetch origin main
git checkout main
git pull origin main
echo "Preparing to create tag $TAG"
if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
echo "Tag $TAG already exists on remote. Refusing to release." >&2
exit 1
fi
git tag -a "$TAG" -m "Version ${TAG#v}"
git push origin "$TAG"
- name: Clean up release branch
if: always()
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ steps.bump_branch.outputs.name }}
shell: bash
run: |
set -euo pipefail
git push origin --delete "$BRANCH" || true
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
generate_release_notes: true
sync_beta:
name: Merge main back into beta via PR
needs:
- bump
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout beta
uses: actions/checkout@v6
with:
ref: beta
fetch-depth: 0
- name: Prepare sync branch from beta with merged main
id: sync_branch
env:
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
# Fetch both branches so we can build a merge commit in CI.
git fetch origin main beta
if git merge-base --is-ancestor origin/main origin/beta; then
echo "beta is already up to date with main. Skipping sync."
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi
SYNC_BRANCH="sync/main-v${NEW_VERSION}-into-beta-${GITHUB_RUN_ID}"
echo "name=$SYNC_BRANCH" >> "$GITHUB_OUTPUT"
echo "skipped=false" >> "$GITHUB_OUTPUT"
git checkout -b "$SYNC_BRANCH" origin/beta
if git merge origin/main --no-ff --no-commit; then
echo "main merged cleanly into sync branch."
else
echo "Merge conflicts detected. Attempting expected conflict resolution for beta version files."
CONFLICTS=$(git diff --name-only --diff-filter=U || true)
if [[ -n "$CONFLICTS" ]]; then
echo "$CONFLICTS"
fi
# Keep beta-side prerelease versions if these files conflict.
for file in MCPForUnity/package.json Server/pyproject.toml; do
if git ls-files -u -- "$file" | grep -q .; then
echo "Keeping beta version for $file"
git checkout --ours -- "$file"
git add "$file"
fi
done
REMAINING=$(git diff --name-only --diff-filter=U || true)
if [[ -n "$REMAINING" ]]; then
echo "Unexpected unresolved conflicts remain:"
echo "$REMAINING"
exit 1
fi
fi
git commit -m "chore: sync main (v${NEW_VERSION}) into beta"
# After releasing X.Y.Z on main, beta should move to X.Y.(Z+1)-beta.1.
IFS='.' read -r MAJOR MINOR PATCH <<< "$NEW_VERSION"
NEXT_PATCH=$((PATCH + 1))
NEXT_BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.1"
echo "beta_version=$NEXT_BETA_VERSION" >> "$GITHUB_OUTPUT"
echo "Setting beta version to $NEXT_BETA_VERSION"
CURRENT_BETA_VERSION=$(jq -r '.version' MCPForUnity/package.json)
if [[ "$CURRENT_BETA_VERSION" != "$NEXT_BETA_VERSION" ]]; then
jq --arg v "$NEXT_BETA_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
mv tmp.json MCPForUnity/package.json
git add MCPForUnity/package.json
git commit -m "chore: set beta version to ${NEXT_BETA_VERSION} after release v${NEW_VERSION}"
else
echo "Beta version already at target: $NEXT_BETA_VERSION"
fi
echo "Pushing sync branch $SYNC_BRANCH"
git push origin "$SYNC_BRANCH"
- name: Create PR to merge sync branch into beta
if: steps.sync_branch.outputs.skipped != 'true'
id: sync_pr
env:
GH_TOKEN: ${{ github.token }}
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
NEXT_BETA_VERSION: ${{ steps.sync_branch.outputs.beta_version }}
SYNC_BRANCH: ${{ steps.sync_branch.outputs.name }}
shell: bash
run: |
set -euo pipefail
PR_URL=$(gh pr create \
--base beta \
--head "$SYNC_BRANCH" \
--title "chore: sync main (v${NEW_VERSION}) into beta" \
--body "Automated sync of main back into beta after release v${NEW_VERSION}, including beta version set to ${NEXT_BETA_VERSION}.")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Merge sync PR
if: steps.sync_branch.outputs.skipped != 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.sync_pr.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
# Best effort: auto-merge if repository settings allow it.
gh pr merge "$PR_NUMBER" --merge --auto --delete-branch || true
# Retry direct merge for up to 2 minutes while checks settle.
for i in {1..24}; do
STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state')
if [[ "$STATE" == "MERGED" ]]; then
echo "Sync PR merged successfully."
exit 0
fi
if gh pr merge "$PR_NUMBER" --merge --delete-branch >/dev/null 2>&1; then
echo "Sync PR merged successfully."
exit 0
fi
echo "Waiting for sync PR to become mergeable... (state: $STATE)"
sleep 5
done
echo "Sync PR did not merge in time."
gh pr view "$PR_NUMBER" --json state,mergeStateStatus,isDraft -q '{state: .state, mergeStateStatus: .mergeStateStatus, isDraft: .isDraft}'
exit 1
publish_docker:
name: Publish Docker image
needs:
- bump
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag }}
fetch-depth: 0
- name: Build and push Docker image
uses: ./.github/actions/publish-docker
with:
docker_username: ${{ secrets.DOCKER_USERNAME }}
docker_password: ${{ secrets.DOCKER_PASSWORD }}
image: ${{ secrets.DOCKER_USERNAME }}/mcp-for-unity-server
version: ${{ needs.bump.outputs.new_version }}
include_branch_tags: "false"
context: .
dockerfile: Server/Dockerfile
platforms: linux/amd64
publish_pypi:
name: Publish Python distribution to PyPI
needs:
- bump
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/mcpforunityserver
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag }}
fetch-depth: 0
# Inlined from .github/actions/publish-pypi to avoid nested composite action issue
# with pypa/gh-action-pypi-publish (see https://github.com/pypa/gh-action-pypi-publish/issues/338)
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "Server/uv.lock"
- name: Build a binary wheel and a source tarball
shell: bash
run: uv build
working-directory: ./Server
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: Server/dist/
publish_mcpb:
name: Generate and publish MCPB bundle
needs:
- bump
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out the repo
uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag }}
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Generate MCPB bundle
env:
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
python3 tools/generate_mcpb.py "$NEW_VERSION" \
--output "unity-mcp-${NEW_VERSION}.mcpb" \
--icon docs/images/coplay-logo.png
- name: Upload MCPB to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.bump.outputs.tag }}
files: unity-mcp-${{ needs.bump.outputs.new_version }}.mcpb
================================================
FILE: .github/workflows/unity-tests.yml
================================================
name: Unity Tests
on:
workflow_dispatch: {}
push:
branches: ["**"]
paths:
- TestProjects/UnityMCPTests/**
- MCPForUnity/Editor/**
- .github/workflows/unity-tests.yml
jobs:
testAllModes:
name: Test in ${{ matrix.testMode }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
projectPath:
- TestProjects/UnityMCPTests
testMode:
- editmode
unityVersion:
- 2021.3.45f2
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true
- name: Detect Unity license secrets
id: detect
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -e
if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then
echo "unity_ok=true" >> "$GITHUB_OUTPUT"
else
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
fi
- name: Skip Unity tests (missing license secrets)
if: steps.detect.outputs.unity_ok != 'true'
run: |
echo "Unity license secrets missing; skipping Unity tests."
- uses: actions/cache@v4
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-${{ matrix.unityVersion }}
restore-keys: |
Library-${{ matrix.projectPath }}-
Library-
# Run domain reload tests first (they're [Explicit] so need explicit category)
- name: Run domain reload tests
if: steps.detect.outputs.unity_ok == 'true'
uses: game-ci/unity-test-runner@v4
id: domain-tests
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: ${{ matrix.testMode }}
customParameters: -testCategory domain_reload
- name: Run tests
if: steps.detect.outputs.unity_ok == 'true'
uses: game-ci/unity-test-runner@v4
id: tests
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: ${{ matrix.testMode }}
- uses: actions/upload-artifact@v4
if: always() && steps.detect.outputs.unity_ok == 'true'
with:
name: Test results for ${{ matrix.testMode }}
path: ${{ steps.tests.outputs.artifactsPath }}
================================================
FILE: .gitignore
================================================
# AI-related files (user-specific)
.cursorrules
.cursorignore
.windsurf
.codeiumignore
.kiro
# Code-copy related files
.clipignore
# Python-generated files
__pycache__/
__pycache__.meta
build/
dist/
wheels/
*.egg-info
# Test coverage
.coverage
.coverage.*
htmlcov/
coverage.xml
*.cover
# Virtual environments
.venv
# Environment files (API keys)
.env
# Unity Editor
*.unitypackage
*.asset
LICENSE.meta
CONTRIBUTING.md.meta
# IDE
.idea/
.vscode/
.aider*
.DS_Store*
# Unity test project lock files
TestProjects/UnityMCPTests/Packages/packages-lock.json
# UnityMCPTests stress-run artifacts (these are created by tests/tools and should never be committed)
TestProjects/UnityMCPTests/Assets/Temp/
# Backup artifacts
*.backup
*.backup.meta
.wt-origin-main/
# CI test reports (generated during test runs)
reports/
# Local testing harness
scripts/local-test/
.claude/settings.local.json
# Ignore the .claude directory, since it might contain local/project-level setting such as deny and allowlist.
/.claude
================================================
FILE: .mcpbignore
================================================
# MCPB Ignore File
# This bundle uses uvx pattern - package downloaded from PyPI at runtime
# Only manifest.json, icon.png, README.md, and LICENSE are needed
# Server source code (downloaded via uvx from PyPI)
Server/
# Unity Client plugin (separate installation)
MCPForUnity/
# Test projects
TestProjects/
# Documentation folder
docs/
# Custom Tools (shipped separately)
CustomTools/
# Development scripts at root
scripts/
tools/
# Claude skill zip (separate distribution)
claude_skill_unity.zip
# Development batch files
deploy-dev.bat
restore-dev.bat
# Test files at root
test_unity_socket_framing.py
mcp_source.py
prune_tool_results.py
# Docker
docker-compose.yml
.dockerignore
Dockerfile
# Chinese README (keep English only)
README-zh.md
# GitHub and CI
.github/
.claude/
# IDE
.vscode/
.idea/
# Python artifacts
*.pyc
__pycache__/
.pytest_cache/
.mypy_cache/
*.egg-info/
dist/
build/
# Environment
.env*
*.local
.venv/
venv/
# Git
.git/
.gitignore
.gitattributes
# Package management
uv.lock
poetry.lock
requirements*.txt
pyproject.toml
# Logs and temp
*.log
*.tmp
.DS_Store
Thumbs.db
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What This Project Is
**MCP for Unity** is a bridge that lets AI assistants (Claude, Cursor, Windsurf, etc.) control the Unity Editor through the Model Context Protocol (MCP). It enables AI-driven game development workflows - creating GameObjects, editing scripts, managing assets, running tests, and more.
## Architecture
```text
AI Assistant (Claude/Cursor)
↓ MCP Protocol (stdio/HTTP)
Python Server (Server/src/)
↓ WebSocket + HTTP
Unity Editor Plugin (MCPForUnity/)
↓ Unity Editor API
Scene, Assets, Scripts
```
**Two codebases, one system:**
- `Server/` - Python MCP server using FastMCP
- `MCPForUnity/` - Unity C# Editor package
### Three Layers on the Python Side
The Python server has three distinct layers. These are **not** auto-generated from each other:
| Layer | Location | Framework | Purpose |
|-------|----------|-----------|---------|
| **MCP Tools** | `Server/src/services/tools/` | FastMCP (`@mcp_for_unity_tool`) | Exposed to AI assistants via MCP protocol |
| **CLI Commands** | `Server/src/cli/commands/` | Click (`@click.command`) | Terminal interface for developers |
| **Resources** | `Server/src/services/resources/` | FastMCP (`@mcp_for_unity_resource`) | Read-only state exposed to AI assistants |
MCP tools call Unity via WebSocket (`send_with_unity_instance`). CLI commands call Unity via HTTP (`run_command`). Both route to the same C# `HandleCommand` methods.
### Transport Modes
- **Stdio**: Single-agent only. Separate Python process per client. Legacy TCP bridge to Unity. New connections stomp old ones.
- **HTTP**: Multi-agent ready. Single shared Python server. WebSocket hub at `/hub/plugin`. Session isolation via `client_id`.
## Code Philosophy
### 1. Domain Symmetry
Python MCP tools mirror C# Editor tools. Each domain exists in both:
- `Server/src/services/tools/manage_material.py` ↔ `MCPForUnity/Editor/Tools/ManageMaterial.cs`
- CLI commands (`Server/src/cli/commands/`) also mirror these but are a separate implementation.
### 2. Minimal Abstraction
Avoid premature abstraction. Three similar lines of code is better than a helper that's used once. Only abstract when you have 3+ genuine use cases.
### 3. Delete Rather Than Deprecate
When removing functionality, delete it completely. No `_unused` renames, no `// removed` comments, no backwards-compatibility shims for internal code.
### 4. Test Coverage Required
Every new feature needs tests. Run them before PRs.
### 5. Keep Tools Focused
Each MCP tool does one thing well. Resist the urge to add "convenient" parameters that bloat the API surface.
### 6. Use Resources for Reading
Keep them smart and focused rather than "read everything" type resources. Resources should be quick and LLM-friendly.
## Key Patterns
### Python MCP Tool Registration
Tools in `Server/src/services/tools/` are auto-discovered. Use the `@mcp_for_unity_tool` decorator:
```python
from services.registry import mcp_for_unity_tool
@mcp_for_unity_tool(
description="Does something in Unity.",
group="core", # core (default), vfx, animation, ui, scripting_ext, testing, probuilder
)
async def manage_something(
ctx: Context,
action: Annotated[Literal["create", "delete"], "Action to perform"],
) -> dict[str, Any]:
unity_instance = await get_unity_instance_from_context(ctx)
params = {"action": action}
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_something", params)
return response
```
The `group` parameter controls tool visibility. Only `"core"` is enabled by default. Non-core groups (vfx, animation, etc.) start disabled and are toggled via `manage_tools`.
### Python CLI Error Handling
CLI commands (not MCP tools) use the `@handle_unity_errors` decorator:
```python
@handle_unity_errors
async def my_command(ctx, ...):
result = await call_unity_tool(...)
```
### C# Tool Registration
Tools are auto-discovered by `CommandRegistry` via reflection. Use the `[McpForUnityTool]` attribute:
```csharp
[McpForUnityTool("manage_something", AutoRegister = false, Group = "core")]
public static class ManageSomething
{
// Sync handler (most tools):
public static object HandleCommand(JObject @params)
{
var p = new ToolParams(@params);
// ...
return new SuccessResponse("Done.", new { data = result });
}
// OR async handler (for long-running operations like play-test, refresh, batch):
public static async Task