#
tokens: 47096/50000 2/263 files (page 14/18)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 14 of 18. Use http://codebase.md/justinpbarnett/unity-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── prompts
│   │   ├── nl-unity-suite-nl.md
│   │   └── nl-unity-suite-t.md
│   └── settings.json
├── .github
│   ├── scripts
│   │   └── mark_skipped.py
│   └── workflows
│       ├── bump-version.yml
│       ├── claude-nl-suite.yml
│       ├── github-repo-stats.yml
│       └── unity-tests.yml
├── .gitignore
├── deploy-dev.bat
├── docs
│   ├── CURSOR_HELP.md
│   ├── CUSTOM_TOOLS.md
│   ├── README-DEV-zh.md
│   ├── README-DEV.md
│   ├── screenshots
│   │   ├── v5_01_uninstall.png
│   │   ├── v5_02_install.png
│   │   ├── v5_03_open_mcp_window.png
│   │   ├── v5_04_rebuild_mcp_server.png
│   │   ├── v5_05_rebuild_success.png
│   │   ├── v6_2_create_python_tools_asset.png
│   │   ├── v6_2_python_tools_asset.png
│   │   ├── v6_new_ui_asset_store_version.png
│   │   ├── v6_new_ui_dark.png
│   │   └── v6_new_ui_light.png
│   ├── TELEMETRY.md
│   ├── v5_MIGRATION.md
│   └── v6_NEW_UI_CHANGES.md
├── LICENSE
├── logo.png
├── mcp_source.py
├── MCPForUnity
│   ├── Editor
│   │   ├── AssemblyInfo.cs
│   │   ├── AssemblyInfo.cs.meta
│   │   ├── Data
│   │   │   ├── DefaultServerConfig.cs
│   │   │   ├── DefaultServerConfig.cs.meta
│   │   │   ├── McpClients.cs
│   │   │   ├── McpClients.cs.meta
│   │   │   ├── PythonToolsAsset.cs
│   │   │   └── PythonToolsAsset.cs.meta
│   │   ├── Data.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
│   │   │   ├── ConfigJsonBuilder.cs
│   │   │   ├── ConfigJsonBuilder.cs.meta
│   │   │   ├── ExecPath.cs
│   │   │   ├── ExecPath.cs.meta
│   │   │   ├── GameObjectSerializer.cs
│   │   │   ├── GameObjectSerializer.cs.meta
│   │   │   ├── McpConfigFileHelper.cs
│   │   │   ├── McpConfigFileHelper.cs.meta
│   │   │   ├── McpConfigurationHelper.cs
│   │   │   ├── McpConfigurationHelper.cs.meta
│   │   │   ├── McpLog.cs
│   │   │   ├── McpLog.cs.meta
│   │   │   ├── McpPathResolver.cs
│   │   │   ├── McpPathResolver.cs.meta
│   │   │   ├── PackageDetector.cs
│   │   │   ├── PackageDetector.cs.meta
│   │   │   ├── PackageInstaller.cs
│   │   │   ├── PackageInstaller.cs.meta
│   │   │   ├── PortManager.cs
│   │   │   ├── PortManager.cs.meta
│   │   │   ├── PythonToolSyncProcessor.cs
│   │   │   ├── PythonToolSyncProcessor.cs.meta
│   │   │   ├── Response.cs
│   │   │   ├── Response.cs.meta
│   │   │   ├── ServerInstaller.cs
│   │   │   ├── ServerInstaller.cs.meta
│   │   │   ├── ServerPathResolver.cs
│   │   │   ├── ServerPathResolver.cs.meta
│   │   │   ├── TelemetryHelper.cs
│   │   │   ├── TelemetryHelper.cs.meta
│   │   │   ├── Vector3Helper.cs
│   │   │   └── Vector3Helper.cs.meta
│   │   ├── Helpers.meta
│   │   ├── Importers
│   │   │   ├── PythonFileImporter.cs
│   │   │   └── PythonFileImporter.cs.meta
│   │   ├── Importers.meta
│   │   ├── MCPForUnity.Editor.asmdef
│   │   ├── MCPForUnity.Editor.asmdef.meta
│   │   ├── MCPForUnityBridge.cs
│   │   ├── MCPForUnityBridge.cs.meta
│   │   ├── Models
│   │   │   ├── Command.cs
│   │   │   ├── Command.cs.meta
│   │   │   ├── McpClient.cs
│   │   │   ├── McpClient.cs.meta
│   │   │   ├── McpConfig.cs
│   │   │   ├── McpConfig.cs.meta
│   │   │   ├── MCPConfigServer.cs
│   │   │   ├── MCPConfigServer.cs.meta
│   │   │   ├── MCPConfigServers.cs
│   │   │   ├── MCPConfigServers.cs.meta
│   │   │   ├── McpStatus.cs
│   │   │   ├── McpStatus.cs.meta
│   │   │   ├── McpTypes.cs
│   │   │   ├── McpTypes.cs.meta
│   │   │   ├── ServerConfig.cs
│   │   │   └── ServerConfig.cs.meta
│   │   ├── Models.meta
│   │   ├── Resources
│   │   │   ├── McpForUnityResourceAttribute.cs
│   │   │   ├── McpForUnityResourceAttribute.cs.meta
│   │   │   ├── MenuItems
│   │   │   │   ├── GetMenuItems.cs
│   │   │   │   └── GetMenuItems.cs.meta
│   │   │   ├── MenuItems.meta
│   │   │   ├── Tests
│   │   │   │   ├── GetTests.cs
│   │   │   │   └── GetTests.cs.meta
│   │   │   └── Tests.meta
│   │   ├── Resources.meta
│   │   ├── Services
│   │   │   ├── BridgeControlService.cs
│   │   │   ├── BridgeControlService.cs.meta
│   │   │   ├── ClientConfigurationService.cs
│   │   │   ├── ClientConfigurationService.cs.meta
│   │   │   ├── IBridgeControlService.cs
│   │   │   ├── IBridgeControlService.cs.meta
│   │   │   ├── IClientConfigurationService.cs
│   │   │   ├── IClientConfigurationService.cs.meta
│   │   │   ├── IPackageUpdateService.cs
│   │   │   ├── IPackageUpdateService.cs.meta
│   │   │   ├── IPathResolverService.cs
│   │   │   ├── IPathResolverService.cs.meta
│   │   │   ├── IPythonToolRegistryService.cs
│   │   │   ├── IPythonToolRegistryService.cs.meta
│   │   │   ├── ITestRunnerService.cs
│   │   │   ├── ITestRunnerService.cs.meta
│   │   │   ├── IToolSyncService.cs
│   │   │   ├── IToolSyncService.cs.meta
│   │   │   ├── MCPServiceLocator.cs
│   │   │   ├── MCPServiceLocator.cs.meta
│   │   │   ├── PackageUpdateService.cs
│   │   │   ├── PackageUpdateService.cs.meta
│   │   │   ├── PathResolverService.cs
│   │   │   ├── PathResolverService.cs.meta
│   │   │   ├── PythonToolRegistryService.cs
│   │   │   ├── PythonToolRegistryService.cs.meta
│   │   │   ├── TestRunnerService.cs
│   │   │   ├── TestRunnerService.cs.meta
│   │   │   ├── ToolSyncService.cs
│   │   │   └── ToolSyncService.cs.meta
│   │   ├── Services.meta
│   │   ├── Setup
│   │   │   ├── SetupWizard.cs
│   │   │   ├── SetupWizard.cs.meta
│   │   │   ├── SetupWizardWindow.cs
│   │   │   └── SetupWizardWindow.cs.meta
│   │   ├── Setup.meta
│   │   ├── Tools
│   │   │   ├── CommandRegistry.cs
│   │   │   ├── CommandRegistry.cs.meta
│   │   │   ├── ExecuteMenuItem.cs
│   │   │   ├── ExecuteMenuItem.cs.meta
│   │   │   ├── ManageAsset.cs
│   │   │   ├── ManageAsset.cs.meta
│   │   │   ├── ManageEditor.cs
│   │   │   ├── ManageEditor.cs.meta
│   │   │   ├── ManageGameObject.cs
│   │   │   ├── ManageGameObject.cs.meta
│   │   │   ├── ManageScene.cs
│   │   │   ├── ManageScene.cs.meta
│   │   │   ├── ManageScript.cs
│   │   │   ├── ManageScript.cs.meta
│   │   │   ├── ManageShader.cs
│   │   │   ├── ManageShader.cs.meta
│   │   │   ├── McpForUnityToolAttribute.cs
│   │   │   ├── McpForUnityToolAttribute.cs.meta
│   │   │   ├── Prefabs
│   │   │   │   ├── ManagePrefabs.cs
│   │   │   │   └── ManagePrefabs.cs.meta
│   │   │   ├── Prefabs.meta
│   │   │   ├── ReadConsole.cs
│   │   │   ├── ReadConsole.cs.meta
│   │   │   ├── RunTests.cs
│   │   │   └── RunTests.cs.meta
│   │   ├── Tools.meta
│   │   ├── Windows
│   │   │   ├── ManualConfigEditorWindow.cs
│   │   │   ├── ManualConfigEditorWindow.cs.meta
│   │   │   ├── MCPForUnityEditorWindow.cs
│   │   │   ├── MCPForUnityEditorWindow.cs.meta
│   │   │   ├── MCPForUnityEditorWindowNew.cs
│   │   │   ├── MCPForUnityEditorWindowNew.cs.meta
│   │   │   ├── MCPForUnityEditorWindowNew.uss
│   │   │   ├── MCPForUnityEditorWindowNew.uss.meta
│   │   │   ├── MCPForUnityEditorWindowNew.uxml
│   │   │   ├── MCPForUnityEditorWindowNew.uxml.meta
│   │   │   ├── VSCodeManualSetupWindow.cs
│   │   │   └── VSCodeManualSetupWindow.cs.meta
│   │   └── Windows.meta
│   ├── Editor.meta
│   ├── package.json
│   ├── package.json.meta
│   ├── README.md
│   ├── README.md.meta
│   ├── Runtime
│   │   ├── MCPForUnity.Runtime.asmdef
│   │   ├── MCPForUnity.Runtime.asmdef.meta
│   │   ├── Serialization
│   │   │   ├── UnityTypeConverters.cs
│   │   │   └── UnityTypeConverters.cs.meta
│   │   └── Serialization.meta
│   ├── Runtime.meta
│   └── UnityMcpServer~
│       └── src
│           ├── __init__.py
│           ├── config.py
│           ├── Dockerfile
│           ├── models.py
│           ├── module_discovery.py
│           ├── port_discovery.py
│           ├── pyproject.toml
│           ├── pyrightconfig.json
│           ├── registry
│           │   ├── __init__.py
│           │   ├── resource_registry.py
│           │   └── tool_registry.py
│           ├── reload_sentinel.py
│           ├── resources
│           │   ├── __init__.py
│           │   ├── menu_items.py
│           │   └── tests.py
│           ├── server_version.txt
│           ├── server.py
│           ├── telemetry_decorator.py
│           ├── telemetry.py
│           ├── test_telemetry.py
│           ├── tools
│           │   ├── __init__.py
│           │   ├── execute_menu_item.py
│           │   ├── manage_asset.py
│           │   ├── manage_editor.py
│           │   ├── manage_gameobject.py
│           │   ├── manage_prefabs.py
│           │   ├── manage_scene.py
│           │   ├── manage_script.py
│           │   ├── manage_shader.py
│           │   ├── read_console.py
│           │   ├── resource_tools.py
│           │   ├── run_tests.py
│           │   └── script_apply_edits.py
│           ├── unity_connection.py
│           └── uv.lock
├── prune_tool_results.py
├── README-zh.md
├── README.md
├── restore-dev.bat
├── scripts
│   └── validate-nlt-coverage.sh
├── test_unity_socket_framing.py
├── TestProjects
│   └── UnityMCPTests
│       ├── .gitignore
│       ├── Assets
│       │   ├── Editor.meta
│       │   ├── Scenes
│       │   │   ├── SampleScene.unity
│       │   │   └── SampleScene.unity.meta
│       │   ├── Scenes.meta
│       │   ├── Scripts
│       │   │   ├── Hello.cs
│       │   │   ├── Hello.cs.meta
│       │   │   ├── LongUnityScriptClaudeTest.cs
│       │   │   ├── LongUnityScriptClaudeTest.cs.meta
│       │   │   ├── TestAsmdef
│       │   │   │   ├── CustomComponent.cs
│       │   │   │   ├── CustomComponent.cs.meta
│       │   │   │   ├── TestAsmdef.asmdef
│       │   │   │   └── TestAsmdef.asmdef.meta
│       │   │   └── TestAsmdef.meta
│       │   ├── Scripts.meta
│       │   ├── Tests
│       │   │   ├── EditMode
│       │   │   │   ├── Data
│       │   │   │   │   ├── PythonToolsAssetTests.cs
│       │   │   │   │   └── PythonToolsAssetTests.cs.meta
│       │   │   │   ├── Data.meta
│       │   │   │   ├── Helpers
│       │   │   │   │   ├── CodexConfigHelperTests.cs
│       │   │   │   │   ├── CodexConfigHelperTests.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
│       │   │   │   │   ├── PackageUpdateServiceTests.cs
│       │   │   │   │   ├── PackageUpdateServiceTests.cs.meta
│       │   │   │   │   ├── PythonToolRegistryServiceTests.cs
│       │   │   │   │   ├── PythonToolRegistryServiceTests.cs.meta
│       │   │   │   │   ├── ToolSyncServiceTests.cs
│       │   │   │   │   └── ToolSyncServiceTests.cs.meta
│       │   │   │   ├── Services.meta
│       │   │   │   ├── Tools
│       │   │   │   │   ├── AIPropertyMatchingTests.cs
│       │   │   │   │   ├── AIPropertyMatchingTests.cs.meta
│       │   │   │   │   ├── CommandRegistryTests.cs
│       │   │   │   │   ├── CommandRegistryTests.cs.meta
│       │   │   │   │   ├── ComponentResolverTests.cs
│       │   │   │   │   ├── ComponentResolverTests.cs.meta
│       │   │   │   │   ├── ExecuteMenuItemTests.cs
│       │   │   │   │   ├── ExecuteMenuItemTests.cs.meta
│       │   │   │   │   ├── ManageGameObjectTests.cs
│       │   │   │   │   ├── ManageGameObjectTests.cs.meta
│       │   │   │   │   ├── ManagePrefabsTests.cs
│       │   │   │   │   ├── ManagePrefabsTests.cs.meta
│       │   │   │   │   ├── ManageScriptValidationTests.cs
│       │   │   │   │   └── ManageScriptValidationTests.cs.meta
│       │   │   │   ├── Tools.meta
│       │   │   │   ├── Windows
│       │   │   │   │   ├── ManualConfigJsonBuilderTests.cs
│       │   │   │   │   └── ManualConfigJsonBuilderTests.cs.meta
│       │   │   │   └── Windows.meta
│       │   │   └── EditMode.meta
│       │   └── Tests.meta
│       ├── Packages
│       │   └── manifest.json
│       └── ProjectSettings
│           ├── Packages
│           │   └── com.unity.testtools.codecoverage
│           │       └── Settings.json
│           └── ProjectVersion.txt
├── tests
│   ├── conftest.py
│   ├── test_edit_normalization_and_noop.py
│   ├── test_edit_strict_and_warnings.py
│   ├── test_find_in_file_minimal.py
│   ├── test_get_sha.py
│   ├── test_improved_anchor_matching.py
│   ├── test_logging_stdout.py
│   ├── test_manage_script_uri.py
│   ├── test_read_console_truncate.py
│   ├── test_read_resource_minimal.py
│   ├── test_resources_api.py
│   ├── test_script_editing.py
│   ├── test_script_tools.py
│   ├── test_telemetry_endpoint_validation.py
│   ├── test_telemetry_queue_worker.py
│   ├── test_telemetry_subaction.py
│   ├── test_transport_framing.py
│   └── test_validate_script_summary.py
├── tools
│   └── stress_mcp.py
└── UnityMcpBridge
    ├── Editor
    │   ├── AssemblyInfo.cs
    │   ├── AssemblyInfo.cs.meta
    │   ├── Data
    │   │   ├── DefaultServerConfig.cs
    │   │   ├── DefaultServerConfig.cs.meta
    │   │   ├── McpClients.cs
    │   │   └── McpClients.cs.meta
    │   ├── Data.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
    │   │   ├── ConfigJsonBuilder.cs
    │   │   ├── ConfigJsonBuilder.cs.meta
    │   │   ├── ExecPath.cs
    │   │   ├── ExecPath.cs.meta
    │   │   ├── GameObjectSerializer.cs
    │   │   ├── GameObjectSerializer.cs.meta
    │   │   ├── McpConfigFileHelper.cs
    │   │   ├── McpConfigFileHelper.cs.meta
    │   │   ├── McpConfigurationHelper.cs
    │   │   ├── McpConfigurationHelper.cs.meta
    │   │   ├── McpLog.cs
    │   │   ├── McpLog.cs.meta
    │   │   ├── McpPathResolver.cs
    │   │   ├── McpPathResolver.cs.meta
    │   │   ├── PackageDetector.cs
    │   │   ├── PackageDetector.cs.meta
    │   │   ├── PackageInstaller.cs
    │   │   ├── PackageInstaller.cs.meta
    │   │   ├── PortManager.cs
    │   │   ├── PortManager.cs.meta
    │   │   ├── Response.cs
    │   │   ├── Response.cs.meta
    │   │   ├── ServerInstaller.cs
    │   │   ├── ServerInstaller.cs.meta
    │   │   ├── ServerPathResolver.cs
    │   │   ├── ServerPathResolver.cs.meta
    │   │   ├── TelemetryHelper.cs
    │   │   ├── TelemetryHelper.cs.meta
    │   │   ├── Vector3Helper.cs
    │   │   └── Vector3Helper.cs.meta
    │   ├── Helpers.meta
    │   ├── MCPForUnity.Editor.asmdef
    │   ├── MCPForUnity.Editor.asmdef.meta
    │   ├── MCPForUnityBridge.cs
    │   ├── MCPForUnityBridge.cs.meta
    │   ├── Models
    │   │   ├── Command.cs
    │   │   ├── Command.cs.meta
    │   │   ├── McpClient.cs
    │   │   ├── McpClient.cs.meta
    │   │   ├── McpConfig.cs
    │   │   ├── McpConfig.cs.meta
    │   │   ├── MCPConfigServer.cs
    │   │   ├── MCPConfigServer.cs.meta
    │   │   ├── MCPConfigServers.cs
    │   │   ├── MCPConfigServers.cs.meta
    │   │   ├── McpStatus.cs
    │   │   ├── McpStatus.cs.meta
    │   │   ├── McpTypes.cs
    │   │   ├── McpTypes.cs.meta
    │   │   ├── ServerConfig.cs
    │   │   └── ServerConfig.cs.meta
    │   ├── Models.meta
    │   ├── Setup
    │   │   ├── SetupWizard.cs
    │   │   ├── SetupWizard.cs.meta
    │   │   ├── SetupWizardWindow.cs
    │   │   └── SetupWizardWindow.cs.meta
    │   ├── Setup.meta
    │   ├── Tools
    │   │   ├── CommandRegistry.cs
    │   │   ├── CommandRegistry.cs.meta
    │   │   ├── ManageAsset.cs
    │   │   ├── ManageAsset.cs.meta
    │   │   ├── ManageEditor.cs
    │   │   ├── ManageEditor.cs.meta
    │   │   ├── ManageGameObject.cs
    │   │   ├── ManageGameObject.cs.meta
    │   │   ├── ManageScene.cs
    │   │   ├── ManageScene.cs.meta
    │   │   ├── ManageScript.cs
    │   │   ├── ManageScript.cs.meta
    │   │   ├── ManageShader.cs
    │   │   ├── ManageShader.cs.meta
    │   │   ├── McpForUnityToolAttribute.cs
    │   │   ├── McpForUnityToolAttribute.cs.meta
    │   │   ├── MenuItems
    │   │   │   ├── ManageMenuItem.cs
    │   │   │   ├── ManageMenuItem.cs.meta
    │   │   │   ├── MenuItemExecutor.cs
    │   │   │   ├── MenuItemExecutor.cs.meta
    │   │   │   ├── MenuItemsReader.cs
    │   │   │   └── MenuItemsReader.cs.meta
    │   │   ├── MenuItems.meta
    │   │   ├── Prefabs
    │   │   │   ├── ManagePrefabs.cs
    │   │   │   └── ManagePrefabs.cs.meta
    │   │   ├── Prefabs.meta
    │   │   ├── ReadConsole.cs
    │   │   └── ReadConsole.cs.meta
    │   ├── Tools.meta
    │   ├── Windows
    │   │   ├── ManualConfigEditorWindow.cs
    │   │   ├── ManualConfigEditorWindow.cs.meta
    │   │   ├── MCPForUnityEditorWindow.cs
    │   │   ├── MCPForUnityEditorWindow.cs.meta
    │   │   ├── VSCodeManualSetupWindow.cs
    │   │   └── VSCodeManualSetupWindow.cs.meta
    │   └── Windows.meta
    ├── Editor.meta
    ├── package.json
    ├── package.json.meta
    ├── README.md
    ├── README.md.meta
    ├── Runtime
    │   ├── MCPForUnity.Runtime.asmdef
    │   ├── MCPForUnity.Runtime.asmdef.meta
    │   ├── Serialization
    │   │   ├── UnityTypeConverters.cs
    │   │   └── UnityTypeConverters.cs.meta
    │   └── Serialization.meta
    ├── Runtime.meta
    └── UnityMcpServer~
        └── src
            ├── __init__.py
            ├── config.py
            ├── Dockerfile
            ├── port_discovery.py
            ├── pyproject.toml
            ├── pyrightconfig.json
            ├── registry
            │   ├── __init__.py
            │   └── tool_registry.py
            ├── reload_sentinel.py
            ├── server_version.txt
            ├── server.py
            ├── telemetry_decorator.py
            ├── telemetry.py
            ├── test_telemetry.py
            ├── tools
            │   ├── __init__.py
            │   ├── manage_asset.py
            │   ├── manage_editor.py
            │   ├── manage_gameobject.py
            │   ├── manage_menu_item.py
            │   ├── manage_prefabs.py
            │   ├── manage_scene.py
            │   ├── manage_script.py
            │   ├── manage_shader.py
            │   ├── read_console.py
            │   ├── resource_tools.py
            │   └── script_apply_edits.py
            ├── unity_connection.py
            └── uv.lock
```

# Files

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/External/Tommy.cs:
--------------------------------------------------------------------------------

```csharp
   1 | #region LICENSE
   2 | 
   3 | /*
   4 |  * MIT License
   5 |  * 
   6 |  * Copyright (c) 2020 Denis Zhidkikh
   7 |  * 
   8 |  * Permission is hereby granted, free of charge, to any person obtaining a copy
   9 |  * of this software and associated documentation files (the "Software"), to deal
  10 |  * in the Software without restriction, including without limitation the rights
  11 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  12 |  * copies of the Software, and to permit persons to whom the Software is
  13 |  * furnished to do so, subject to the following conditions:
  14 |  * 
  15 |  * The above copyright notice and this permission notice shall be included in all
  16 |  * copies or substantial portions of the Software.
  17 |  * 
  18 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21 |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  24 |  * SOFTWARE.
  25 |  */
  26 | 
  27 | #endregion
  28 | 
  29 | using System;
  30 | using System.Collections;
  31 | using System.Collections.Generic;
  32 | using System.Globalization;
  33 | using System.IO;
  34 | using System.Linq;
  35 | using System.Text;
  36 | using System.Text.RegularExpressions;
  37 | 
  38 | namespace MCPForUnity.External.Tommy
  39 | {
  40 |     #region TOML Nodes
  41 | 
  42 |     public abstract class TomlNode : IEnumerable
  43 |     {
  44 |         public virtual bool HasValue { get; } = false;
  45 |         public virtual bool IsArray { get; } = false;
  46 |         public virtual bool IsTable { get; } = false;
  47 |         public virtual bool IsString { get; } = false;
  48 |         public virtual bool IsInteger { get; } = false;
  49 |         public virtual bool IsFloat { get; } = false;
  50 |         public bool IsDateTime => IsDateTimeLocal || IsDateTimeOffset;
  51 |         public virtual bool IsDateTimeLocal { get; } = false;
  52 |         public virtual bool IsDateTimeOffset { get; } = false;
  53 |         public virtual bool IsBoolean { get; } = false;
  54 |         public virtual string Comment { get; set; }
  55 |         public virtual int CollapseLevel { get; set; }
  56 | 
  57 |         public virtual TomlTable AsTable => this as TomlTable;
  58 |         public virtual TomlString AsString => this as TomlString;
  59 |         public virtual TomlInteger AsInteger => this as TomlInteger;
  60 |         public virtual TomlFloat AsFloat => this as TomlFloat;
  61 |         public virtual TomlBoolean AsBoolean => this as TomlBoolean;
  62 |         public virtual TomlDateTimeLocal AsDateTimeLocal => this as TomlDateTimeLocal;
  63 |         public virtual TomlDateTimeOffset AsDateTimeOffset => this as TomlDateTimeOffset;
  64 |         public virtual TomlDateTime AsDateTime => this as TomlDateTime;
  65 |         public virtual TomlArray AsArray => this as TomlArray;
  66 | 
  67 |         public virtual int ChildrenCount => 0;
  68 | 
  69 |         public virtual TomlNode this[string key]
  70 |         {
  71 |             get => null;
  72 |             set { }
  73 |         }
  74 | 
  75 |         public virtual TomlNode this[int index]
  76 |         {
  77 |             get => null;
  78 |             set { }
  79 |         }
  80 | 
  81 |         public virtual IEnumerable<TomlNode> Children
  82 |         {
  83 |             get { yield break; }
  84 |         }
  85 | 
  86 |         public virtual IEnumerable<string> Keys
  87 |         {
  88 |             get { yield break; }
  89 |         }
  90 | 
  91 |         public IEnumerator GetEnumerator() => Children.GetEnumerator();
  92 | 
  93 |         public virtual bool TryGetNode(string key, out TomlNode node)
  94 |         {
  95 |             node = null;
  96 |             return false;
  97 |         }
  98 | 
  99 |         public virtual bool HasKey(string key) => false;
 100 | 
 101 |         public virtual bool HasItemAt(int index) => false;
 102 | 
 103 |         public virtual void Add(string key, TomlNode node) { }
 104 | 
 105 |         public virtual void Add(TomlNode node) { }
 106 | 
 107 |         public virtual void Delete(TomlNode node) { }
 108 | 
 109 |         public virtual void Delete(string key) { }
 110 | 
 111 |         public virtual void Delete(int index) { }
 112 | 
 113 |         public virtual void AddRange(IEnumerable<TomlNode> nodes)
 114 |         {
 115 |             foreach (var tomlNode in nodes) Add(tomlNode);
 116 |         }
 117 | 
 118 |         public virtual void WriteTo(TextWriter tw, string name = null) => tw.WriteLine(ToInlineToml());
 119 | 
 120 |         public virtual string ToInlineToml() => ToString();
 121 | 
 122 |         #region Native type to TOML cast
 123 | 
 124 |         public static implicit operator TomlNode(string value) => new TomlString { Value = value };
 125 | 
 126 |         public static implicit operator TomlNode(bool value) => new TomlBoolean { Value = value };
 127 | 
 128 |         public static implicit operator TomlNode(long value) => new TomlInteger { Value = value };
 129 | 
 130 |         public static implicit operator TomlNode(float value) => new TomlFloat { Value = value };
 131 | 
 132 |         public static implicit operator TomlNode(double value) => new TomlFloat { Value = value };
 133 | 
 134 |         public static implicit operator TomlNode(DateTime value) => new TomlDateTimeLocal { Value = value };
 135 | 
 136 |         public static implicit operator TomlNode(DateTimeOffset value) => new TomlDateTimeOffset { Value = value };
 137 | 
 138 |         public static implicit operator TomlNode(TomlNode[] nodes)
 139 |         {
 140 |             var result = new TomlArray();
 141 |             result.AddRange(nodes);
 142 |             return result;
 143 |         }
 144 | 
 145 |         #endregion
 146 | 
 147 |         #region TOML to native type cast
 148 | 
 149 |         public static implicit operator string(TomlNode value) => value.ToString();
 150 | 
 151 |         public static implicit operator int(TomlNode value) => (int)value.AsInteger.Value;
 152 | 
 153 |         public static implicit operator long(TomlNode value) => value.AsInteger.Value;
 154 | 
 155 |         public static implicit operator float(TomlNode value) => (float)value.AsFloat.Value;
 156 | 
 157 |         public static implicit operator double(TomlNode value) => value.AsFloat.Value;
 158 | 
 159 |         public static implicit operator bool(TomlNode value) => value.AsBoolean.Value;
 160 | 
 161 |         public static implicit operator DateTime(TomlNode value) => value.AsDateTimeLocal.Value;
 162 | 
 163 |         public static implicit operator DateTimeOffset(TomlNode value) => value.AsDateTimeOffset.Value;
 164 | 
 165 |         #endregion
 166 |     }
 167 | 
 168 |     public class TomlString : TomlNode
 169 |     {
 170 |         public override bool HasValue { get; } = true;
 171 |         public override bool IsString { get; } = true;
 172 |         public bool IsMultiline { get; set; }
 173 |         public bool MultilineTrimFirstLine { get; set; }
 174 |         public bool PreferLiteral { get; set; }
 175 | 
 176 |         public string Value { get; set; }
 177 | 
 178 |         public override string ToString() => Value;
 179 | 
 180 |         public override string ToInlineToml()
 181 |         {
 182 |             // Automatically convert literal to non-literal if there are too many literal string symbols
 183 |             if (Value.IndexOf(new string(TomlSyntax.LITERAL_STRING_SYMBOL, IsMultiline ? 3 : 1), StringComparison.Ordinal) != -1 && PreferLiteral) PreferLiteral = false;
 184 |             var quotes = new string(PreferLiteral ? TomlSyntax.LITERAL_STRING_SYMBOL : TomlSyntax.BASIC_STRING_SYMBOL,
 185 |                                     IsMultiline ? 3 : 1);
 186 |             var result = PreferLiteral ? Value : Value.Escape(!IsMultiline);
 187 |             if (IsMultiline)
 188 |                 result = result.Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
 189 |             if (IsMultiline && (MultilineTrimFirstLine || !MultilineTrimFirstLine && result.StartsWith(Environment.NewLine)))
 190 |                 result = $"{Environment.NewLine}{result}";
 191 |             return $"{quotes}{result}{quotes}";
 192 |         }
 193 |     }
 194 | 
 195 |     public class TomlInteger : TomlNode
 196 |     {
 197 |         public enum Base
 198 |         {
 199 |             Binary = 2,
 200 |             Octal = 8,
 201 |             Decimal = 10,
 202 |             Hexadecimal = 16
 203 |         }
 204 | 
 205 |         public override bool IsInteger { get; } = true;
 206 |         public override bool HasValue { get; } = true;
 207 |         public Base IntegerBase { get; set; } = Base.Decimal;
 208 | 
 209 |         public long Value { get; set; }
 210 | 
 211 |         public override string ToString() => Value.ToString();
 212 | 
 213 |         public override string ToInlineToml() =>
 214 |             IntegerBase != Base.Decimal
 215 |                 ? $"0{TomlSyntax.BaseIdentifiers[(int)IntegerBase]}{Convert.ToString(Value, (int)IntegerBase)}"
 216 |                 : Value.ToString(CultureInfo.InvariantCulture);
 217 |     }
 218 | 
 219 |     public class TomlFloat : TomlNode, IFormattable
 220 |     {
 221 |         public override bool IsFloat { get; } = true;
 222 |         public override bool HasValue { get; } = true;
 223 | 
 224 |         public double Value { get; set; }
 225 | 
 226 |         public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
 227 | 
 228 |         public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider);
 229 | 
 230 |         public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);
 231 | 
 232 |         public override string ToInlineToml() =>
 233 |             Value switch
 234 |             {
 235 |                 var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE,
 236 |                 var v when double.IsPositiveInfinity(v) => TomlSyntax.INF_VALUE,
 237 |                 var v when double.IsNegativeInfinity(v) => TomlSyntax.NEG_INF_VALUE,
 238 |                 var v => v.ToString("G", CultureInfo.InvariantCulture).ToLowerInvariant()
 239 |             };
 240 |     }
 241 | 
 242 |     public class TomlBoolean : TomlNode
 243 |     {
 244 |         public override bool IsBoolean { get; } = true;
 245 |         public override bool HasValue { get; } = true;
 246 | 
 247 |         public bool Value { get; set; }
 248 | 
 249 |         public override string ToString() => Value.ToString();
 250 | 
 251 |         public override string ToInlineToml() => Value ? TomlSyntax.TRUE_VALUE : TomlSyntax.FALSE_VALUE;
 252 |     }
 253 | 
 254 |     public class TomlDateTime : TomlNode, IFormattable
 255 |     {
 256 |         public int SecondsPrecision { get; set; }
 257 |         public override bool HasValue { get; } = true;
 258 |         public virtual string ToString(string format, IFormatProvider formatProvider) => string.Empty;
 259 |         public virtual string ToString(IFormatProvider formatProvider) => string.Empty;
 260 |         protected virtual string ToInlineTomlInternal() => string.Empty;
 261 | 
 262 |         public override string ToInlineToml() => ToInlineTomlInternal()
 263 |                                                 .Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator)
 264 |                                                 .Replace(TomlSyntax.ISO861ZeroZone, TomlSyntax.RFC3339ZeroZone);
 265 |     }
 266 | 
 267 |     public class TomlDateTimeOffset : TomlDateTime
 268 |     {
 269 |         public override bool IsDateTimeOffset { get; } = true;
 270 |         public DateTimeOffset Value { get; set; }
 271 | 
 272 |         public override string ToString() => Value.ToString(CultureInfo.CurrentCulture);
 273 |         public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);
 274 | 
 275 |         public override string ToString(string format, IFormatProvider formatProvider) =>
 276 |             Value.ToString(format, formatProvider);
 277 | 
 278 |         protected override string ToInlineTomlInternal() => Value.ToString(TomlSyntax.RFC3339Formats[SecondsPrecision]);
 279 |     }
 280 | 
 281 |     public class TomlDateTimeLocal : TomlDateTime
 282 |     {
 283 |         public enum DateTimeStyle
 284 |         {
 285 |             Date,
 286 |             Time,
 287 |             DateTime
 288 |         }
 289 | 
 290 |         public override bool IsDateTimeLocal { get; } = true;
 291 |         public DateTimeStyle Style { get; set; } = DateTimeStyle.DateTime;
 292 |         public DateTime Value { get; set; }
 293 | 
 294 |         public override string ToString() => Value.ToString(CultureInfo.CurrentCulture);
 295 | 
 296 |         public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);
 297 | 
 298 |         public override string ToString(string format, IFormatProvider formatProvider) =>
 299 |             Value.ToString(format, formatProvider);
 300 | 
 301 |         public override string ToInlineToml() =>
 302 |             Style switch
 303 |             {
 304 |                 DateTimeStyle.Date => Value.ToString(TomlSyntax.LocalDateFormat),
 305 |                 DateTimeStyle.Time => Value.ToString(TomlSyntax.RFC3339LocalTimeFormats[SecondsPrecision]),
 306 |                 var _ => Value.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision])
 307 |             };
 308 |     }
 309 | 
 310 |     public class TomlArray : TomlNode
 311 |     {
 312 |         private List<TomlNode> values;
 313 | 
 314 |         public override bool HasValue { get; } = true;
 315 |         public override bool IsArray { get; } = true;
 316 |         public bool IsMultiline { get; set; }
 317 |         public bool IsTableArray { get; set; }
 318 |         public List<TomlNode> RawArray => values ??= new List<TomlNode>();
 319 | 
 320 |         public override TomlNode this[int index]
 321 |         {
 322 |             get
 323 |             {
 324 |                 if (index < RawArray.Count) return RawArray[index];
 325 |                 var lazy = new TomlLazy(this);
 326 |                 this[index] = lazy;
 327 |                 return lazy;
 328 |             }
 329 |             set
 330 |             {
 331 |                 if (index == RawArray.Count)
 332 |                     RawArray.Add(value);
 333 |                 else
 334 |                     RawArray[index] = value;
 335 |             }
 336 |         }
 337 | 
 338 |         public override int ChildrenCount => RawArray.Count;
 339 | 
 340 |         public override IEnumerable<TomlNode> Children => RawArray.AsEnumerable();
 341 | 
 342 |         public override void Add(TomlNode node) => RawArray.Add(node);
 343 | 
 344 |         public override void AddRange(IEnumerable<TomlNode> nodes) => RawArray.AddRange(nodes);
 345 | 
 346 |         public override void Delete(TomlNode node) => RawArray.Remove(node);
 347 | 
 348 |         public override void Delete(int index) => RawArray.RemoveAt(index);
 349 | 
 350 |         public override string ToString() => ToString(false);
 351 | 
 352 |         public string ToString(bool multiline)
 353 |         {
 354 |             var sb = new StringBuilder();
 355 |             sb.Append(TomlSyntax.ARRAY_START_SYMBOL);
 356 |             if (ChildrenCount != 0)
 357 |             {
 358 |                 var arrayStart = multiline ? $"{Environment.NewLine}  " : " ";
 359 |                 var arraySeparator = multiline ? $"{TomlSyntax.ITEM_SEPARATOR}{Environment.NewLine}  " : $"{TomlSyntax.ITEM_SEPARATOR} ";
 360 |                 var arrayEnd = multiline ? Environment.NewLine : " ";
 361 |                 sb.Append(arrayStart)
 362 |                   .Append(arraySeparator.Join(RawArray.Select(n => n.ToInlineToml())))
 363 |                   .Append(arrayEnd);
 364 |             }
 365 |             sb.Append(TomlSyntax.ARRAY_END_SYMBOL);
 366 |             return sb.ToString();
 367 |         }
 368 | 
 369 |         public override void WriteTo(TextWriter tw, string name = null)
 370 |         {
 371 |             // If it's a normal array, write it as usual
 372 |             if (!IsTableArray)
 373 |             {
 374 |                 tw.WriteLine(ToString(IsMultiline));
 375 |                 return;
 376 |             }
 377 | 
 378 |             if (!(Comment is null))
 379 |             {
 380 |                 tw.WriteLine();
 381 |                 Comment.AsComment(tw);
 382 |             }
 383 |             tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
 384 |             tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
 385 |             tw.Write(name);
 386 |             tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
 387 |             tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
 388 |             tw.WriteLine();
 389 | 
 390 |             var first = true;
 391 | 
 392 |             foreach (var tomlNode in RawArray)
 393 |             {
 394 |                 if (!(tomlNode is TomlTable tbl))
 395 |                     throw new TomlFormatException("The array is marked as array table but contains non-table nodes!");
 396 | 
 397 |                 // Ensure it's parsed as a section
 398 |                 tbl.IsInline = false;
 399 | 
 400 |                 if (!first)
 401 |                 {
 402 |                     tw.WriteLine();
 403 | 
 404 |                     Comment?.AsComment(tw);
 405 |                     tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
 406 |                     tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
 407 |                     tw.Write(name);
 408 |                     tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
 409 |                     tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
 410 |                     tw.WriteLine();
 411 |                 }
 412 | 
 413 |                 first = false;
 414 | 
 415 |                 // Don't write section since it's already written here
 416 |                 tbl.WriteTo(tw, name, false);
 417 |             }
 418 |         }
 419 |     }
 420 | 
 421 |     public class TomlTable : TomlNode
 422 |     {
 423 |         private Dictionary<string, TomlNode> children;
 424 |         internal bool isImplicit;
 425 | 
 426 |         public override bool HasValue { get; } = false;
 427 |         public override bool IsTable { get; } = true;
 428 |         public bool IsInline { get; set; }
 429 |         public Dictionary<string, TomlNode> RawTable => children ??= new Dictionary<string, TomlNode>();
 430 | 
 431 |         public override TomlNode this[string key]
 432 |         {
 433 |             get
 434 |             {
 435 |                 if (RawTable.TryGetValue(key, out var result)) return result;
 436 |                 var lazy = new TomlLazy(this);
 437 |                 RawTable[key] = lazy;
 438 |                 return lazy;
 439 |             }
 440 |             set => RawTable[key] = value;
 441 |         }
 442 | 
 443 |         public override int ChildrenCount => RawTable.Count;
 444 |         public override IEnumerable<TomlNode> Children => RawTable.Select(kv => kv.Value);
 445 |         public override IEnumerable<string> Keys => RawTable.Select(kv => kv.Key);
 446 |         public override bool HasKey(string key) => RawTable.ContainsKey(key);
 447 |         public override void Add(string key, TomlNode node) => RawTable.Add(key, node);
 448 |         public override bool TryGetNode(string key, out TomlNode node) => RawTable.TryGetValue(key, out node);
 449 |         public override void Delete(TomlNode node) => RawTable.Remove(RawTable.First(kv => kv.Value == node).Key);
 450 |         public override void Delete(string key) => RawTable.Remove(key);
 451 | 
 452 |         public override string ToString()
 453 |         {
 454 |             var sb = new StringBuilder();
 455 |             sb.Append(TomlSyntax.INLINE_TABLE_START_SYMBOL);
 456 | 
 457 |             if (ChildrenCount != 0)
 458 |             {
 459 |                 var collapsed = CollectCollapsedItems(normalizeOrder: false);
 460 | 
 461 |                 if (collapsed.Count != 0)
 462 |                     sb.Append(' ')
 463 |                       .Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(collapsed.Select(n =>
 464 |                                                                        $"{n.Key} {TomlSyntax.KEY_VALUE_SEPARATOR} {n.Value.ToInlineToml()}")));
 465 |                 sb.Append(' ');
 466 |             }
 467 | 
 468 |             sb.Append(TomlSyntax.INLINE_TABLE_END_SYMBOL);
 469 |             return sb.ToString();
 470 |         }
 471 | 
 472 |         private LinkedList<KeyValuePair<string, TomlNode>> CollectCollapsedItems(string prefix = "", int level = 0, bool normalizeOrder = true)
 473 |         {
 474 |             var nodes = new LinkedList<KeyValuePair<string, TomlNode>>();
 475 |             var postNodes = normalizeOrder ? new LinkedList<KeyValuePair<string, TomlNode>>() : nodes;
 476 | 
 477 |             foreach (var keyValuePair in RawTable)
 478 |             {
 479 |                 var node = keyValuePair.Value;
 480 |                 var key = keyValuePair.Key.AsKey();
 481 | 
 482 |                 if (node is TomlTable tbl)
 483 |                 {
 484 |                     var subnodes = tbl.CollectCollapsedItems($"{prefix}{key}.", level + 1, normalizeOrder);
 485 |                     // Write main table first before writing collapsed items
 486 |                     if (subnodes.Count == 0 && node.CollapseLevel == level)
 487 |                     {
 488 |                         postNodes.AddLast(new KeyValuePair<string, TomlNode>($"{prefix}{key}", node));
 489 |                     }
 490 |                     foreach (var kv in subnodes)
 491 |                         postNodes.AddLast(kv);
 492 |                 }
 493 |                 else if (node.CollapseLevel == level)
 494 |                     nodes.AddLast(new KeyValuePair<string, TomlNode>($"{prefix}{key}", node));
 495 |             }
 496 | 
 497 |             if (normalizeOrder)
 498 |                 foreach (var kv in postNodes)
 499 |                     nodes.AddLast(kv);
 500 | 
 501 |             return nodes;
 502 |         }
 503 | 
 504 |         public override void WriteTo(TextWriter tw, string name = null) => WriteTo(tw, name, true);
 505 | 
 506 |         internal void WriteTo(TextWriter tw, string name, bool writeSectionName)
 507 |         {
 508 |             // The table is inline table
 509 |             if (IsInline && name != null)
 510 |             {
 511 |                 tw.WriteLine(ToInlineToml());
 512 |                 return;
 513 |             }
 514 | 
 515 |             var collapsedItems = CollectCollapsedItems();
 516 | 
 517 |             if (collapsedItems.Count == 0)
 518 |                 return;
 519 | 
 520 |             var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } or TomlArray { IsTableArray: true });
 521 | 
 522 |             Comment?.AsComment(tw);
 523 | 
 524 |             if (name != null && (hasRealValues || Comment != null) && writeSectionName)
 525 |             {
 526 |                 tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
 527 |                 tw.Write(name);
 528 |                 tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
 529 |                 tw.WriteLine();
 530 |             }
 531 |             else if (Comment != null) // Add some spacing between the first node and the comment
 532 |             {
 533 |                 tw.WriteLine();
 534 |             }
 535 | 
 536 |             var namePrefix = name == null ? "" : $"{name}.";
 537 |             var first = true;
 538 | 
 539 |             foreach (var collapsedItem in collapsedItems)
 540 |             {
 541 |                 var key = collapsedItem.Key;
 542 |                 if (collapsedItem.Value is TomlArray { IsTableArray: true } or TomlTable { IsInline: false })
 543 |                 {
 544 |                     if (!first) tw.WriteLine();
 545 |                     first = false;
 546 |                     collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}");
 547 |                     continue;
 548 |                 }
 549 |                 first = false;
 550 | 
 551 |                 collapsedItem.Value.Comment?.AsComment(tw);
 552 |                 tw.Write(key);
 553 |                 tw.Write(' ');
 554 |                 tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR);
 555 |                 tw.Write(' ');
 556 | 
 557 |                 collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}");
 558 |             }
 559 |         }
 560 |     }
 561 | 
 562 |     internal class TomlLazy : TomlNode
 563 |     {
 564 |         private readonly TomlNode parent;
 565 |         private TomlNode replacement;
 566 | 
 567 |         public TomlLazy(TomlNode parent) => this.parent = parent;
 568 | 
 569 |         public override TomlNode this[int index]
 570 |         {
 571 |             get => Set<TomlArray>()[index];
 572 |             set => Set<TomlArray>()[index] = value;
 573 |         }
 574 | 
 575 |         public override TomlNode this[string key]
 576 |         {
 577 |             get => Set<TomlTable>()[key];
 578 |             set => Set<TomlTable>()[key] = value;
 579 |         }
 580 | 
 581 |         public override void Add(TomlNode node) => Set<TomlArray>().Add(node);
 582 | 
 583 |         public override void Add(string key, TomlNode node) => Set<TomlTable>().Add(key, node);
 584 | 
 585 |         public override void AddRange(IEnumerable<TomlNode> nodes) => Set<TomlArray>().AddRange(nodes);
 586 | 
 587 |         private TomlNode Set<T>() where T : TomlNode, new()
 588 |         {
 589 |             if (replacement != null) return replacement;
 590 | 
 591 |             var newNode = new T
 592 |             {
 593 |                 Comment = Comment
 594 |             };
 595 | 
 596 |             if (parent.IsTable)
 597 |             {
 598 |                 var key = parent.Keys.FirstOrDefault(s => parent.TryGetNode(s, out var node) && node.Equals(this));
 599 |                 if (key == null) return default(T);
 600 | 
 601 |                 parent[key] = newNode;
 602 |             }
 603 |             else if (parent.IsArray)
 604 |             {
 605 |                 var index = parent.Children.TakeWhile(child => child != this).Count();
 606 |                 if (index == parent.ChildrenCount) return default(T);
 607 |                 parent[index] = newNode;
 608 |             }
 609 |             else
 610 |             {
 611 |                 return default(T);
 612 |             }
 613 | 
 614 |             replacement = newNode;
 615 |             return newNode;
 616 |         }
 617 |     }
 618 | 
 619 |     #endregion
 620 | 
 621 |     #region Parser
 622 | 
 623 |     public class TOMLParser : IDisposable
 624 |     {
 625 |         public enum ParseState
 626 |         {
 627 |             None,
 628 |             KeyValuePair,
 629 |             SkipToNextLine,
 630 |             Table
 631 |         }
 632 | 
 633 |         private readonly TextReader reader;
 634 |         private ParseState currentState;
 635 |         private int line, col;
 636 |         private List<TomlSyntaxException> syntaxErrors;
 637 | 
 638 |         public TOMLParser(TextReader reader)
 639 |         {
 640 |             this.reader = reader;
 641 |             line = col = 0;
 642 |         }
 643 | 
 644 |         public bool ForceASCII { get; set; }
 645 | 
 646 |         public void Dispose() => reader?.Dispose();
 647 | 
 648 |         public TomlTable Parse()
 649 |         {
 650 |             syntaxErrors = new List<TomlSyntaxException>();
 651 |             line = col = 1;
 652 |             var rootNode = new TomlTable();
 653 |             var currentNode = rootNode;
 654 |             currentState = ParseState.None;
 655 |             var keyParts = new List<string>();
 656 |             var arrayTable = false;
 657 |             StringBuilder latestComment = null;
 658 |             var firstComment = true;
 659 | 
 660 |             int currentChar;
 661 |             while ((currentChar = reader.Peek()) >= 0)
 662 |             {
 663 |                 var c = (char)currentChar;
 664 | 
 665 |                 if (currentState == ParseState.None)
 666 |                 {
 667 |                     // Skip white space
 668 |                     if (TomlSyntax.IsWhiteSpace(c)) goto consume_character;
 669 | 
 670 |                     if (TomlSyntax.IsNewLine(c))
 671 |                     {
 672 |                         // Check if there are any comments and so far no items being declared
 673 |                         if (latestComment != null && firstComment)
 674 |                         {
 675 |                             rootNode.Comment = latestComment.ToString().TrimEnd();
 676 |                             latestComment = null;
 677 |                             firstComment = false;
 678 |                         }
 679 | 
 680 |                         if (TomlSyntax.IsLineBreak(c))
 681 |                             AdvanceLine();
 682 | 
 683 |                         goto consume_character;
 684 |                     }
 685 | 
 686 |                     // Start of a comment; ignore until newline
 687 |                     if (c == TomlSyntax.COMMENT_SYMBOL)
 688 |                     {
 689 |                         latestComment ??= new StringBuilder();
 690 |                         latestComment.AppendLine(ParseComment());
 691 |                         AdvanceLine(1);
 692 |                         continue;
 693 |                     }
 694 | 
 695 |                     // Encountered a non-comment value. The comment must belong to it (ignore possible newlines)!
 696 |                     firstComment = false;
 697 | 
 698 |                     if (c == TomlSyntax.TABLE_START_SYMBOL)
 699 |                     {
 700 |                         currentState = ParseState.Table;
 701 |                         goto consume_character;
 702 |                     }
 703 | 
 704 |                     if (TomlSyntax.IsBareKey(c) || TomlSyntax.IsQuoted(c))
 705 |                     {
 706 |                         currentState = ParseState.KeyValuePair;
 707 |                     }
 708 |                     else
 709 |                     {
 710 |                         AddError($"Unexpected character \"{c}\"");
 711 |                         continue;
 712 |                     }
 713 |                 }
 714 | 
 715 |                 if (currentState == ParseState.KeyValuePair)
 716 |                 {
 717 |                     var keyValuePair = ReadKeyValuePair(keyParts);
 718 | 
 719 |                     if (keyValuePair == null)
 720 |                     {
 721 |                         latestComment = null;
 722 |                         keyParts.Clear();
 723 | 
 724 |                         if (currentState != ParseState.None)
 725 |                             AddError("Failed to parse key-value pair!");
 726 |                         continue;
 727 |                     }
 728 | 
 729 |                     keyValuePair.Comment = latestComment?.ToString()?.TrimEnd();
 730 |                     var inserted = InsertNode(keyValuePair, currentNode, keyParts);
 731 |                     latestComment = null;
 732 |                     keyParts.Clear();
 733 |                     if (inserted)
 734 |                         currentState = ParseState.SkipToNextLine;
 735 |                     continue;
 736 |                 }
 737 | 
 738 |                 if (currentState == ParseState.Table)
 739 |                 {
 740 |                     if (keyParts.Count == 0)
 741 |                     {
 742 |                         // We have array table
 743 |                         if (c == TomlSyntax.TABLE_START_SYMBOL)
 744 |                         {
 745 |                             // Consume the character
 746 |                             ConsumeChar();
 747 |                             arrayTable = true;
 748 |                         }
 749 | 
 750 |                         if (!ReadKeyName(ref keyParts, TomlSyntax.TABLE_END_SYMBOL))
 751 |                         {
 752 |                             keyParts.Clear();
 753 |                             continue;
 754 |                         }
 755 | 
 756 |                         if (keyParts.Count == 0)
 757 |                         {
 758 |                             AddError("Table name is emtpy.");
 759 |                             arrayTable = false;
 760 |                             latestComment = null;
 761 |                             keyParts.Clear();
 762 |                         }
 763 | 
 764 |                         continue;
 765 |                     }
 766 | 
 767 |                     if (c == TomlSyntax.TABLE_END_SYMBOL)
 768 |                     {
 769 |                         if (arrayTable)
 770 |                         {
 771 |                             // Consume the ending bracket so we can peek the next character
 772 |                             ConsumeChar();
 773 |                             var nextChar = reader.Peek();
 774 |                             if (nextChar < 0 || (char)nextChar != TomlSyntax.TABLE_END_SYMBOL)
 775 |                             {
 776 |                                 AddError($"Array table {".".Join(keyParts)} has only one closing bracket.");
 777 |                                 keyParts.Clear();
 778 |                                 arrayTable = false;
 779 |                                 latestComment = null;
 780 |                                 continue;
 781 |                             }
 782 |                         }
 783 | 
 784 |                         currentNode = CreateTable(rootNode, keyParts, arrayTable);
 785 |                         if (currentNode != null)
 786 |                         {
 787 |                             currentNode.IsInline = false;
 788 |                             currentNode.Comment = latestComment?.ToString()?.TrimEnd();
 789 |                         }
 790 | 
 791 |                         keyParts.Clear();
 792 |                         arrayTable = false;
 793 |                         latestComment = null;
 794 | 
 795 |                         if (currentNode == null)
 796 |                         {
 797 |                             if (currentState != ParseState.None)
 798 |                                 AddError("Error creating table array!");
 799 |                             // Reset a node to root in order to try and continue parsing
 800 |                             currentNode = rootNode;
 801 |                             continue;
 802 |                         }
 803 | 
 804 |                         currentState = ParseState.SkipToNextLine;
 805 |                         goto consume_character;
 806 |                     }
 807 | 
 808 |                     if (keyParts.Count != 0)
 809 |                     {
 810 |                         AddError($"Unexpected character \"{c}\"");
 811 |                         keyParts.Clear();
 812 |                         arrayTable = false;
 813 |                         latestComment = null;
 814 |                     }
 815 |                 }
 816 | 
 817 |                 if (currentState == ParseState.SkipToNextLine)
 818 |                 {
 819 |                     if (TomlSyntax.IsWhiteSpace(c) || c == TomlSyntax.NEWLINE_CARRIAGE_RETURN_CHARACTER)
 820 |                         goto consume_character;
 821 | 
 822 |                     if (c is TomlSyntax.COMMENT_SYMBOL or TomlSyntax.NEWLINE_CHARACTER)
 823 |                     {
 824 |                         currentState = ParseState.None;
 825 |                         AdvanceLine();
 826 | 
 827 |                         if (c == TomlSyntax.COMMENT_SYMBOL)
 828 |                         {
 829 |                             col++;
 830 |                             ParseComment();
 831 |                             continue;
 832 |                         }
 833 | 
 834 |                         goto consume_character;
 835 |                     }
 836 | 
 837 |                     AddError($"Unexpected character \"{c}\" at the end of the line.");
 838 |                 }
 839 | 
 840 |             consume_character:
 841 |                 reader.Read();
 842 |                 col++;
 843 |             }
 844 | 
 845 |             if (currentState != ParseState.None && currentState != ParseState.SkipToNextLine)
 846 |                 AddError("Unexpected end of file!");
 847 | 
 848 |             if (syntaxErrors.Count > 0)
 849 |                 throw new TomlParseException(rootNode, syntaxErrors);
 850 | 
 851 |             return rootNode;
 852 |         }
 853 | 
 854 |         private bool AddError(string message, bool skipLine = true)
 855 |         {
 856 |             syntaxErrors.Add(new TomlSyntaxException(message, currentState, line, col));
 857 |             // Skip the whole line in hope that it was only a single faulty value (and non-multiline one at that)
 858 |             if (skipLine)
 859 |             {
 860 |                 reader.ReadLine();
 861 |                 AdvanceLine(1);
 862 |             }
 863 |             currentState = ParseState.None;
 864 |             return false;
 865 |         }
 866 | 
 867 |         private void AdvanceLine(int startCol = 0)
 868 |         {
 869 |             line++;
 870 |             col = startCol;
 871 |         }
 872 | 
 873 |         private int ConsumeChar()
 874 |         {
 875 |             col++;
 876 |             return reader.Read();
 877 |         }
 878 | 
 879 |         #region Key-Value pair parsing
 880 | 
 881 |         /**
 882 |          * Reads a single key-value pair.
 883 |          * Assumes the cursor is at the first character that belong to the pair (including possible whitespace).
 884 |          * Consumes all characters that belong to the key and the value (ignoring possible trailing whitespace at the end).
 885 |          * 
 886 |          * Example:
 887 |          * foo = "bar"  ==> foo = "bar"
 888 |          * ^                           ^
 889 |          */
 890 |         private TomlNode ReadKeyValuePair(List<string> keyParts)
 891 |         {
 892 |             int cur;
 893 |             while ((cur = reader.Peek()) >= 0)
 894 |             {
 895 |                 var c = (char)cur;
 896 | 
 897 |                 if (TomlSyntax.IsQuoted(c) || TomlSyntax.IsBareKey(c))
 898 |                 {
 899 |                     if (keyParts.Count != 0)
 900 |                     {
 901 |                         AddError("Encountered extra characters in key definition!");
 902 |                         return null;
 903 |                     }
 904 | 
 905 |                     if (!ReadKeyName(ref keyParts, TomlSyntax.KEY_VALUE_SEPARATOR))
 906 |                         return null;
 907 | 
 908 |                     continue;
 909 |                 }
 910 | 
 911 |                 if (TomlSyntax.IsWhiteSpace(c))
 912 |                 {
 913 |                     ConsumeChar();
 914 |                     continue;
 915 |                 }
 916 | 
 917 |                 if (c == TomlSyntax.KEY_VALUE_SEPARATOR)
 918 |                 {
 919 |                     ConsumeChar();
 920 |                     return ReadValue();
 921 |                 }
 922 | 
 923 |                 AddError($"Unexpected character \"{c}\" in key name.");
 924 |                 return null;
 925 |             }
 926 | 
 927 |             return null;
 928 |         }
 929 | 
 930 |         /**
 931 |          * Reads a single value.
 932 |          * Assumes the cursor is at the first character that belongs to the value (including possible starting whitespace).
 933 |          * Consumes all characters belonging to the value (ignoring possible trailing whitespace at the end).
 934 |          * 
 935 |          * Example:
 936 |          * "test"  ==> "test"
 937 |          * ^                 ^
 938 |          */
 939 |         private TomlNode ReadValue(bool skipNewlines = false)
 940 |         {
 941 |             int cur;
 942 |             while ((cur = reader.Peek()) >= 0)
 943 |             {
 944 |                 var c = (char)cur;
 945 | 
 946 |                 if (TomlSyntax.IsWhiteSpace(c))
 947 |                 {
 948 |                     ConsumeChar();
 949 |                     continue;
 950 |                 }
 951 | 
 952 |                 if (c == TomlSyntax.COMMENT_SYMBOL)
 953 |                 {
 954 |                     AddError("No value found!");
 955 |                     return null;
 956 |                 }
 957 | 
 958 |                 if (TomlSyntax.IsNewLine(c))
 959 |                 {
 960 |                     if (skipNewlines)
 961 |                     {
 962 |                         reader.Read();
 963 |                         AdvanceLine(1);
 964 |                         continue;
 965 |                     }
 966 | 
 967 |                     AddError("Encountered a newline when expecting a value!");
 968 |                     return null;
 969 |                 }
 970 | 
 971 |                 if (TomlSyntax.IsQuoted(c))
 972 |                 {
 973 |                     var isMultiline = IsTripleQuote(c, out var excess);
 974 | 
 975 |                     // Error occurred in triple quote parsing
 976 |                     if (currentState == ParseState.None)
 977 |                         return null;
 978 | 
 979 |                     var value = isMultiline
 980 |                         ? ReadQuotedValueMultiLine(c)
 981 |                         : ReadQuotedValueSingleLine(c, excess);
 982 | 
 983 |                     if (value is null)
 984 |                         return null;
 985 | 
 986 |                     return new TomlString
 987 |                     {
 988 |                         Value = value,
 989 |                         IsMultiline = isMultiline,
 990 |                         PreferLiteral = c == TomlSyntax.LITERAL_STRING_SYMBOL
 991 |                     };
 992 |                 }
 993 | 
 994 |                 return c switch
 995 |                 {
 996 |                     TomlSyntax.INLINE_TABLE_START_SYMBOL => ReadInlineTable(),
 997 |                     TomlSyntax.ARRAY_START_SYMBOL => ReadArray(),
 998 |                     var _ => ReadTomlValue()
 999 |                 };
1000 |             }
1001 | 
1002 |             return null;
1003 |         }
1004 | 
1005 |         /**
1006 |          * Reads a single key name.
1007 |          * Assumes the cursor is at the first character belonging to the key (with possible trailing whitespace if `skipWhitespace = true`).
1008 |          * Consumes all the characters until the `until` character is met (but does not consume the character itself).
1009 |          * 
1010 |          * Example 1:
1011 |          * foo.bar  ==>  foo.bar           (`skipWhitespace = false`, `until = ' '`)
1012 |          * ^                    ^
1013 |          * 
1014 |          * Example 2:
1015 |          * [ foo . bar ] ==>  [ foo . bar ]     (`skipWhitespace = true`, `until = ']'`)
1016 |          * ^                             ^
1017 |          */
1018 |         private bool ReadKeyName(ref List<string> parts, char until)
1019 |         {
1020 |             var buffer = new StringBuilder();
1021 |             var quoted = false;
1022 |             var prevWasSpace = false;
1023 |             int cur;
1024 |             while ((cur = reader.Peek()) >= 0)
1025 |             {
1026 |                 var c = (char)cur;
1027 | 
1028 |                 // Reached the final character
1029 |                 if (c == until) break;
1030 | 
1031 |                 if (TomlSyntax.IsWhiteSpace(c))
1032 |                 {
1033 |                     prevWasSpace = true;
1034 |                     goto consume_character;
1035 |                 }
1036 | 
1037 |                 if (buffer.Length == 0) prevWasSpace = false;
1038 | 
1039 |                 if (c == TomlSyntax.SUBKEY_SEPARATOR)
1040 |                 {
1041 |                     if (buffer.Length == 0 && !quoted)
1042 |                         return AddError($"Found an extra subkey separator in {".".Join(parts)}...");
1043 | 
1044 |                     parts.Add(buffer.ToString());
1045 |                     buffer.Length = 0;
1046 |                     quoted = false;
1047 |                     prevWasSpace = false;
1048 |                     goto consume_character;
1049 |                 }
1050 | 
1051 |                 if (prevWasSpace)
1052 |                     return AddError("Invalid spacing in key name");
1053 | 
1054 |                 if (TomlSyntax.IsQuoted(c))
1055 |                 {
1056 |                     if (quoted)
1057 | 
1058 |                         return AddError("Expected a subkey separator but got extra data instead!");
1059 | 
1060 |                     if (buffer.Length != 0)
1061 |                         return AddError("Encountered a quote in the middle of subkey name!");
1062 | 
1063 |                     // Consume the quote character and read the key name
1064 |                     col++;
1065 |                     buffer.Append(ReadQuotedValueSingleLine((char)reader.Read()));
1066 |                     quoted = true;
1067 |                     continue;
1068 |                 }
1069 | 
1070 |                 if (TomlSyntax.IsBareKey(c))
1071 |                 {
1072 |                     buffer.Append(c);
1073 |                     goto consume_character;
1074 |                 }
1075 | 
1076 |                 // If we see an invalid symbol, let the next parser handle it
1077 |                 break;
1078 | 
1079 |             consume_character:
1080 |                 reader.Read();
1081 |                 col++;
1082 |             }
1083 | 
1084 |             if (buffer.Length == 0 && !quoted)
1085 |                 return AddError($"Found an extra subkey separator in {".".Join(parts)}...");
1086 | 
1087 |             parts.Add(buffer.ToString());
1088 | 
1089 |             return true;
1090 |         }
1091 | 
1092 |         #endregion
1093 | 
1094 |         #region Non-string value parsing
1095 | 
1096 |         /**
1097 |          * Reads the whole raw value until the first non-value character is encountered.
1098 |          * Assumes the cursor start position at the first value character and consumes all characters that may be related to the value.
1099 |          * Example:
1100 |          * 
1101 |          * 1_0_0_0  ==>  1_0_0_0
1102 |          * ^                    ^
1103 |          */
1104 |         private string ReadRawValue()
1105 |         {
1106 |             var result = new StringBuilder();
1107 |             int cur;
1108 |             while ((cur = reader.Peek()) >= 0)
1109 |             {
1110 |                 var c = (char)cur;
1111 |                 if (c == TomlSyntax.COMMENT_SYMBOL || TomlSyntax.IsNewLine(c) || TomlSyntax.IsValueSeparator(c)) break;
1112 |                 result.Append(c);
1113 |                 ConsumeChar();
1114 |             }
1115 | 
1116 |             // Replace trim with manual space counting?
1117 |             return result.ToString().Trim();
1118 |         }
1119 | 
1120 |         /**
1121 |          * Reads and parses a non-string, non-composite TOML value.
1122 |          * Assumes the cursor at the first character that is related to the value (with possible spaces).
1123 |          * Consumes all the characters that are related to the value.
1124 |          * 
1125 |          * Example
1126 |          * 1_0_0_0 # This is a comment
1127 |          * <newline>
1128 |          *     ==>  1_0_0_0 # This is a comment
1129 |          *     ^                                                  ^
1130 |          */
1131 |         private TomlNode ReadTomlValue()
1132 |         {
1133 |             var value = ReadRawValue();
1134 |             TomlNode node = value switch
1135 |             {
1136 |                 var v when TomlSyntax.IsBoolean(v) => bool.Parse(v),
1137 |                 var v when TomlSyntax.IsNaN(v) => double.NaN,
1138 |                 var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity,
1139 |                 var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity,
1140 |                 var v when TomlSyntax.IsInteger(v) => long.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR),
1141 |                                                                  CultureInfo.InvariantCulture),
1142 |                 var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR),
1143 |                                                                  CultureInfo.InvariantCulture),
1144 |                 var v when TomlSyntax.IsIntegerWithBase(v, out var numberBase) => new TomlInteger
1145 |                 {
1146 |                     Value = Convert.ToInt64(value.Substring(2).RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), numberBase),
1147 |                     IntegerBase = (TomlInteger.Base)numberBase
1148 |                 },
1149 |                 var _ => null
1150 |             };
1151 |             if (node != null) return node;
1152 | 
1153 |             // Normalize by removing space separator
1154 |             value = value.Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator);
1155 |             if (StringUtils.TryParseDateTime<DateTime>(value,
1156 |                                              TomlSyntax.RFC3339LocalDateTimeFormats,
1157 |                                              DateTimeStyles.AssumeLocal,
1158 |                                              DateTime.TryParseExact,
1159 |                                              out var dateTimeResult,
1160 |                                              out var precision))
1161 |                 return new TomlDateTimeLocal
1162 |                 {
1163 |                     Value = dateTimeResult,
1164 |                     SecondsPrecision = precision
1165 |                 };
1166 | 
1167 |             if (DateTime.TryParseExact(value,
1168 |                                        TomlSyntax.LocalDateFormat,
1169 |                                        CultureInfo.InvariantCulture,
1170 |                                        DateTimeStyles.AssumeLocal,
1171 |                                        out dateTimeResult))
1172 |                 return new TomlDateTimeLocal
1173 |                 {
1174 |                     Value = dateTimeResult,
1175 |                     Style = TomlDateTimeLocal.DateTimeStyle.Date
1176 |                 };
1177 | 
1178 |             if (StringUtils.TryParseDateTime(value,
1179 |                                              TomlSyntax.RFC3339LocalTimeFormats,
1180 |                                              DateTimeStyles.AssumeLocal,
1181 |                                              DateTime.TryParseExact,
1182 |                                              out dateTimeResult,
1183 |                                              out precision))
1184 |                 return new TomlDateTimeLocal
1185 |                 {
1186 |                     Value = dateTimeResult,
1187 |                     Style = TomlDateTimeLocal.DateTimeStyle.Time,
1188 |                     SecondsPrecision = precision
1189 |                 };
1190 | 
1191 |             if (StringUtils.TryParseDateTime<DateTimeOffset>(value,
1192 |                                                              TomlSyntax.RFC3339Formats,
1193 |                                                              DateTimeStyles.None,
1194 |                                                              DateTimeOffset.TryParseExact,
1195 |                                                              out var dateTimeOffsetResult,
1196 |                                                              out precision))
1197 |                 return new TomlDateTimeOffset
1198 |                 {
1199 |                     Value = dateTimeOffsetResult,
1200 |                     SecondsPrecision = precision
1201 |                 };
1202 | 
1203 |             AddError($"Value \"{value}\" is not a valid TOML value!");
1204 |             return null;
1205 |         }
1206 | 
1207 |         /**
1208 |          * Reads an array value.
1209 |          * Assumes the cursor is at the start of the array definition. Reads all character until the array closing bracket.
1210 |          * 
1211 |          * Example:
1212 |          * [1, 2, 3]  ==>  [1, 2, 3]
1213 |          * ^                        ^
1214 |          */
1215 |         private TomlArray ReadArray()
1216 |         {
1217 |             // Consume the start of array character
1218 |             ConsumeChar();
1219 |             var result = new TomlArray();
1220 |             TomlNode currentValue = null;
1221 |             var expectValue = true;
1222 | 
1223 |             int cur;
1224 |             while ((cur = reader.Peek()) >= 0)
1225 |             {
1226 |                 var c = (char)cur;
1227 | 
1228 |                 if (c == TomlSyntax.ARRAY_END_SYMBOL)
1229 |                 {
1230 |                     ConsumeChar();
1231 |                     break;
1232 |                 }
1233 | 
1234 |                 if (c == TomlSyntax.COMMENT_SYMBOL)
1235 |                 {
1236 |                     reader.ReadLine();
1237 |                     AdvanceLine(1);
1238 |                     continue;
1239 |                 }
1240 | 
1241 |                 if (TomlSyntax.IsWhiteSpace(c) || TomlSyntax.IsNewLine(c))
1242 |                 {
1243 |                     if (TomlSyntax.IsLineBreak(c))
1244 |                         AdvanceLine();
1245 |                     goto consume_character;
1246 |                 }
1247 | 
1248 |                 if (c == TomlSyntax.ITEM_SEPARATOR)
1249 |                 {
1250 |                     if (currentValue == null)
1251 |                     {
1252 |                         AddError("Encountered multiple value separators");
1253 |                         return null;
1254 |                     }
1255 | 
1256 |                     result.Add(currentValue);
1257 |                     currentValue = null;
1258 |                     expectValue = true;
1259 |                     goto consume_character;
1260 |                 }
1261 | 
1262 |                 if (!expectValue)
1263 |                 {
1264 |                     AddError("Missing separator between values");
1265 |                     return null;
1266 |                 }
1267 |                 currentValue = ReadValue(true);
1268 |                 if (currentValue == null)
1269 |                 {
1270 |                     if (currentState != ParseState.None)
1271 |                         AddError("Failed to determine and parse a value!");
1272 |                     return null;
1273 |                 }
1274 |                 expectValue = false;
1275 | 
1276 |                 continue;
1277 |             consume_character:
1278 |                 ConsumeChar();
1279 |             }
1280 | 
1281 |             if (currentValue != null) result.Add(currentValue);
1282 |             return result;
1283 |         }
1284 | 
1285 |         /**
1286 |          * Reads an inline table.
1287 |          * Assumes the cursor is at the start of the table definition. Reads all character until the table closing bracket.
1288 |          * 
1289 |          * Example:
1290 |          * { test = "foo", value = 1 }  ==>  { test = "foo", value = 1 }
1291 |          * ^                                                            ^
1292 |          */
1293 |         private TomlNode ReadInlineTable()
1294 |         {
1295 |             ConsumeChar();
1296 |             var result = new TomlTable { IsInline = true };
1297 |             TomlNode currentValue = null;
1298 |             var separator = false;
1299 |             var keyParts = new List<string>();
1300 |             int cur;
1301 |             while ((cur = reader.Peek()) >= 0)
1302 |             {
1303 |                 var c = (char)cur;
1304 | 
1305 |                 if (c == TomlSyntax.INLINE_TABLE_END_SYMBOL)
1306 |                 {
1307 |                     ConsumeChar();
1308 |                     break;
1309 |                 }
1310 | 
1311 |                 if (c == TomlSyntax.COMMENT_SYMBOL)
1312 |                 {
1313 |                     AddError("Incomplete inline table definition!");
1314 |                     return null;
1315 |                 }
1316 | 
1317 |                 if (TomlSyntax.IsNewLine(c))
1318 |                 {
1319 |                     AddError("Inline tables are only allowed to be on single line");
1320 |                     return null;
1321 |                 }
1322 | 
1323 |                 if (TomlSyntax.IsWhiteSpace(c))
1324 |                     goto consume_character;
1325 | 
1326 |                 if (c == TomlSyntax.ITEM_SEPARATOR)
1327 |                 {
1328 |                     if (currentValue == null)
1329 |                     {
1330 |                         AddError("Encountered multiple value separators in inline table!");
1331 |                         return null;
1332 |                     }
1333 | 
1334 |                     if (!InsertNode(currentValue, result, keyParts))
1335 |                         return null;
1336 |                     keyParts.Clear();
1337 |                     currentValue = null;
1338 |                     separator = true;
1339 |                     goto consume_character;
1340 |                 }
1341 | 
1342 |                 separator = false;
1343 |                 currentValue = ReadKeyValuePair(keyParts);
1344 |                 continue;
1345 | 
1346 |             consume_character:
1347 |                 ConsumeChar();
1348 |             }
1349 | 
1350 |             if (separator)
1351 |             {
1352 |                 AddError("Trailing commas are not allowed in inline tables.");
1353 |                 return null;
1354 |             }
1355 | 
1356 |             if (currentValue != null && !InsertNode(currentValue, result, keyParts))
1357 |                 return null;
1358 | 
1359 |             return result;
1360 |         }
1361 | 
1362 |         #endregion
1363 | 
1364 |         #region String parsing
1365 | 
1366 |         /**
1367 |          * Checks if the string value a multiline string (i.e. a triple quoted string).
1368 |          * Assumes the cursor is at the first quote character. Consumes the least amount of characters needed to determine if the string is multiline.
1369 |          * 
1370 |          * If the result is false, returns the consumed character through the `excess` variable.
1371 |          * 
1372 |          * Example 1:
1373 |          * """test"""  ==>  """test"""
1374 |          * ^                   ^
1375 |          * 
1376 |          * Example 2:
1377 |          * "test"  ==>  "test"         (doesn't return the first quote)
1378 |          * ^             ^
1379 |          * 
1380 |          * Example 3:
1381 |          * ""  ==>  ""        (returns the extra `"` through the `excess` variable)
1382 |          * ^          ^
1383 |          */
1384 |         private bool IsTripleQuote(char quote, out char excess)
1385 |         {
1386 |             // Copypasta, but it's faster...
1387 | 
1388 |             int cur;
1389 |             // Consume the first quote
1390 |             ConsumeChar();
1391 |             if ((cur = reader.Peek()) < 0)
1392 |             {
1393 |                 excess = '\0';
1394 |                 return AddError("Unexpected end of file!");
1395 |             }
1396 | 
1397 |             if ((char)cur != quote)
1398 |             {
1399 |                 excess = '\0';
1400 |                 return false;
1401 |             }
1402 | 
1403 |             // Consume the second quote
1404 |             excess = (char)ConsumeChar();
1405 |             if ((cur = reader.Peek()) < 0 || (char)cur != quote) return false;
1406 | 
1407 |             // Consume the final quote
1408 |             ConsumeChar();
1409 |             excess = '\0';
1410 |             return true;
1411 |         }
1412 | 
1413 |         /**
1414 |          * A convenience method to process a single character within a quote.
1415 |          */
1416 |         private bool ProcessQuotedValueCharacter(char quote,
1417 |                                                  bool isNonLiteral,
1418 |                                                  char c,
1419 |                                                  StringBuilder sb,
1420 |                                                  ref bool escaped)
1421 |         {
1422 |             if (TomlSyntax.MustBeEscaped(c))
1423 |                 return AddError($"The character U+{(int)c:X8} must be escaped in a string!");
1424 | 
1425 |             if (escaped)
1426 |             {
1427 |                 sb.Append(c);
1428 |                 escaped = false;
1429 |                 return false;
1430 |             }
1431 | 
1432 |             if (c == quote)
1433 |             {
1434 |                 if (!isNonLiteral && reader.Peek() == quote)
1435 |                 {
1436 |                     reader.Read();
1437 |                     col++;
1438 |                     sb.Append(quote);
1439 |                     return false;
1440 |                 }
1441 | 
1442 |                 return true;
1443 |             }
1444 |             if (isNonLiteral && c == TomlSyntax.ESCAPE_SYMBOL)
1445 |                 escaped = true;
1446 |             if (c == TomlSyntax.NEWLINE_CHARACTER)
1447 |                 return AddError("Encountered newline in single line string!");
1448 | 
1449 |             sb.Append(c);
1450 |             return false;
1451 |         }
1452 | 
1453 |         /**
1454 |          * Reads a single-line string.
1455 |          * Assumes the cursor is at the first character that belongs to the string.
1456 |          * Consumes all characters that belong to the string (including the closing quote).
1457 |          * 
1458 |          * Example:
1459 |          * "test"  ==>  "test"
1460 |          * ^                 ^
1461 |          */
1462 |         private string ReadQuotedValueSingleLine(char quote, char initialData = '\0')
1463 |         {
1464 |             var isNonLiteral = quote == TomlSyntax.BASIC_STRING_SYMBOL;
1465 |             var sb = new StringBuilder();
1466 |             var escaped = false;
1467 | 
1468 |             if (initialData != '\0')
1469 |             {
1470 |                 var shouldReturn =
1471 |                     ProcessQuotedValueCharacter(quote, isNonLiteral, initialData, sb, ref escaped);
1472 |                 if (currentState == ParseState.None) return null;
1473 |                 if (shouldReturn)
1474 |                     if (isNonLiteral)
1475 |                     {
1476 |                         if (sb.ToString().TryUnescape(out var res, out var ex)) return res;
1477 |                         AddError(ex.Message);
1478 |                         return null;
1479 |                     }
1480 |                     else
1481 |                         return sb.ToString();
1482 |             }
1483 | 
1484 |             int cur;
1485 |             var readDone = false;
1486 |             while ((cur = reader.Read()) >= 0)
1487 |             {
1488 |                 // Consume the character
1489 |                 col++;
1490 |                 var c = (char)cur;
1491 |                 readDone = ProcessQuotedValueCharacter(quote, isNonLiteral, c, sb, ref escaped);
1492 |                 if (readDone)
1493 |                 {
1494 |                     if (currentState == ParseState.None) return null;
1495 |                     break;
1496 |                 }
1497 |             }
1498 | 
1499 |             if (!readDone)
1500 |             {
1501 |                 AddError("Unclosed string.");
1502 |                 return null;
1503 |             }
1504 | 
1505 |             if (!isNonLiteral) return sb.ToString();
1506 |             if (sb.ToString().TryUnescape(out var unescaped, out var unescapedEx)) return unescaped;
1507 |             AddError(unescapedEx.Message);
1508 |             return null;
1509 |         }
1510 | 
1511 |         /**
1512 |          * Reads a multiline string.
1513 |          * Assumes the cursor is at the first character that belongs to the string.
1514 |          * Consumes all characters that belong to the string and the three closing quotes.
1515 |          * 
1516 |          * Example:
1517 |          * """test"""  ==>  """test"""
1518 |          * ^                       ^
1519 |          */
1520 |         private string ReadQuotedValueMultiLine(char quote)
1521 |         {
1522 |             var isBasic = quote == TomlSyntax.BASIC_STRING_SYMBOL;
1523 |             var sb = new StringBuilder();
1524 |             var escaped = false;
1525 |             var skipWhitespace = false;
1526 |             var skipWhitespaceLineSkipped = false;
1527 |             var quotesEncountered = 0;
1528 |             var first = true;
1529 |             int cur;
1530 |             while ((cur = ConsumeChar()) >= 0)
1531 |             {
1532 |                 var c = (char)cur;
1533 |                 if (TomlSyntax.MustBeEscaped(c, true))
1534 |                 {
1535 |                     AddError($"The character U+{(int)c:X8} must be escaped!");
1536 |                     return null;
1537 |                 }
1538 |                 // Trim the first newline
1539 |                 if (first && TomlSyntax.IsNewLine(c))
1540 |                 {
1541 |                     if (TomlSyntax.IsLineBreak(c))
1542 |                         first = false;
1543 |                     else
1544 |                         AdvanceLine();
1545 |                     continue;
1546 |                 }
1547 | 
1548 |                 first = false;
1549 |                 //TODO: Reuse ProcessQuotedValueCharacter
1550 |                 // Skip the current character if it is going to be escaped later
1551 |                 if (escaped)
1552 |                 {
1553 |                     sb.Append(c);
1554 |                     escaped = false;
1555 |                     continue;
1556 |                 }
1557 | 
1558 |                 // If we are currently skipping empty spaces, skip
1559 |                 if (skipWhitespace)
1560 |                 {
1561 |                     if (TomlSyntax.IsEmptySpace(c))
1562 |                     {
1563 |                         if (TomlSyntax.IsLineBreak(c))
1564 |                         {
1565 |                             skipWhitespaceLineSkipped = true;
1566 |                             AdvanceLine();
1567 |                         }
1568 |                         continue;
1569 |                     }
1570 | 
1571 |                     if (!skipWhitespaceLineSkipped)
1572 |                     {
1573 |                         AddError("Non-whitespace character after trim marker.");
1574 |                         return null;
1575 |                     }
1576 | 
1577 |                     skipWhitespaceLineSkipped = false;
1578 |                     skipWhitespace = false;
1579 |                 }
1580 | 
1581 |                 // If we encounter an escape sequence...
1582 |                 if (isBasic && c == TomlSyntax.ESCAPE_SYMBOL)
1583 |                 {
1584 |                     var next = reader.Peek();
1585 |                     var nc = (char)next;
1586 |                     if (next >= 0)
1587 |                     {
1588 |                         // ...and the next char is empty space, we must skip all whitespaces
1589 |                         if (TomlSyntax.IsEmptySpace(nc))
1590 |                         {
1591 |                             skipWhitespace = true;
1592 |                             continue;
1593 |                         }
1594 | 
1595 |                         // ...and we have \" or \, skip the character
1596 |                         if (nc == quote || nc == TomlSyntax.ESCAPE_SYMBOL) escaped = true;
1597 |                     }
1598 |                 }
1599 | 
1600 |                 // Count the consecutive quotes
1601 |                 if (c == quote)
1602 |                     quotesEncountered++;
1603 |                 else
1604 |                     quotesEncountered = 0;
1605 | 
1606 |                 // If the are three quotes, count them as closing quotes
1607 |                 if (quotesEncountered == 3) break;
1608 | 
1609 |                 sb.Append(c);
1610 |             }
1611 | 
1612 |             // TOML actually allows to have five ending quotes like
1613 |             // """"" => "" belong to the string + """ is the actual ending
1614 |             quotesEncountered = 0;
1615 |             while ((cur = reader.Peek()) >= 0)
1616 |             {
1617 |                 var c = (char)cur;
1618 |                 if (c == quote && ++quotesEncountered < 3)
1619 |                 {
1620 |                     sb.Append(c);
1621 |                     ConsumeChar();
1622 |                 }
1623 |                 else break;
1624 |             }
1625 | 
1626 |             // Remove last two quotes (third one wasn't included by default)
1627 |             sb.Length -= 2;
1628 |             if (!isBasic) return sb.ToString();
1629 |             if (sb.ToString().TryUnescape(out var res, out var ex)) return res;
1630 |             AddError(ex.Message);
1631 |             return null;
1632 |         }
1633 | 
1634 |         #endregion
1635 | 
1636 |         #region Node creation
1637 | 
1638 |         private bool InsertNode(TomlNode node, TomlNode root, IList<string> path)
1639 |         {
1640 |             var latestNode = root;
1641 |             if (path.Count > 1)
1642 |                 for (var index = 0; index < path.Count - 1; index++)
1643 |                 {
1644 |                     var subkey = path[index];
1645 |                     if (latestNode.TryGetNode(subkey, out var currentNode))
1646 |                     {
1647 |                         if (currentNode.HasValue)
1648 |                             return AddError($"The key {".".Join(path)} already has a value assigned to it!");
1649 |                     }
1650 |                     else
1651 |                     {
1652 |                         currentNode = new TomlTable();
1653 |                         latestNode[subkey] = currentNode;
1654 |                     }
1655 | 
1656 |                     latestNode = currentNode;
1657 |                     if (latestNode is TomlTable { IsInline: true })
1658 |                         return AddError($"Cannot assign {".".Join(path)} because it will edit an immutable table.");
1659 |                 }
1660 | 
1661 |             if (latestNode.HasKey(path[path.Count - 1]))
1662 |                 return AddError($"The key {".".Join(path)} is already defined!");
1663 |             latestNode[path[path.Count - 1]] = node;
1664 |             node.CollapseLevel = path.Count - 1;
1665 |             return true;
1666 |         }
1667 | 
1668 |         private TomlTable CreateTable(TomlNode root, IList<string> path, bool arrayTable)
1669 |         {
1670 |             if (path.Count == 0) return null;
1671 |             var latestNode = root;
1672 |             for (var index = 0; index < path.Count; index++)
1673 |             {
1674 |                 var subkey = path[index];
1675 | 
1676 |                 if (latestNode.TryGetNode(subkey, out var node))
1677 |                 {
1678 |                     if (node.IsArray && arrayTable)
1679 |                     {
1680 |                         var arr = (TomlArray)node;
1681 | 
1682 |                         if (!arr.IsTableArray)
1683 |                         {
1684 |                             AddError($"The array {".".Join(path)} cannot be redefined as an array table!");
1685 |                             return null;
1686 |                         }
1687 | 
1688 |                         if (index == path.Count - 1)
1689 |                         {
1690 |                             latestNode = new TomlTable();
1691 |                             arr.Add(latestNode);
1692 |                             break;
1693 |                         }
1694 | 
1695 |                         latestNode = arr[arr.ChildrenCount - 1];
1696 |                         continue;
1697 |                     }
1698 | 
1699 |                     if (node is TomlTable { IsInline: true })
1700 |                     {
1701 |                         AddError($"Cannot create table {".".Join(path)} because it will edit an immutable table.");
1702 |                         return null;
1703 |                     }
1704 | 
1705 |                     if (node.HasValue)
1706 |                     {
1707 |                         if (!(node is TomlArray { IsTableArray: true } array))
1708 |                         {
1709 |                             AddError($"The key {".".Join(path)} has a value assigned to it!");
1710 |                             return null;
1711 |                         }
1712 | 
1713 |                         latestNode = array[array.ChildrenCount - 1];
1714 |                         continue;
1715 |                     }
1716 | 
1717 |                     if (index == path.Count - 1)
1718 |                     {
1719 |                         if (arrayTable && !node.IsArray)
1720 |                         {
1721 |                             AddError($"The table {".".Join(path)} cannot be redefined as an array table!");
1722 |                             return null;
1723 |                         }
1724 | 
1725 |                         if (node is TomlTable { isImplicit: false })
1726 |                         {
1727 |                             AddError($"The table {".".Join(path)} is defined multiple times!");
1728 |                             return null;
1729 |                         }
1730 |                     }
1731 |                 }
1732 |                 else
1733 |                 {
1734 |                     if (index == path.Count - 1 && arrayTable)
1735 |                     {
1736 |                         var table = new TomlTable();
1737 |                         var arr = new TomlArray
1738 |                         {
1739 |                             IsTableArray = true
1740 |                         };
1741 |                         arr.Add(table);
1742 |                         latestNode[subkey] = arr;
1743 |                         latestNode = table;
1744 |                         break;
1745 |                     }
1746 | 
1747 |                     node = new TomlTable { isImplicit = true };
1748 |                     latestNode[subkey] = node;
1749 |                 }
1750 | 
1751 |                 latestNode = node;
1752 |             }
1753 | 
1754 |             var result = (TomlTable)latestNode;
1755 |             result.isImplicit = false;
1756 |             return result;
1757 |         }
1758 | 
1759 |         #endregion
1760 | 
1761 |         #region Misc parsing
1762 | 
1763 |         private string ParseComment()
1764 |         {
1765 |             ConsumeChar();
1766 |             var commentLine = reader.ReadLine()?.Trim() ?? "";
1767 |             if (commentLine.Any(ch => TomlSyntax.MustBeEscaped(ch)))
1768 |                 AddError("Comment must not contain control characters other than tab.", false);
1769 |             return commentLine;
1770 |         }
1771 |         #endregion
1772 |     }
1773 | 
1774 |     #endregion
1775 | 
1776 |     public static class TOML
1777 |     {
1778 |         public static bool ForceASCII { get; set; } = false;
1779 | 
1780 |         public static TomlTable Parse(TextReader reader)
1781 |         {
1782 |             using var parser = new TOMLParser(reader) { ForceASCII = ForceASCII };
1783 |             return parser.Parse();
1784 |         }
1785 |     }
1786 | 
1787 |     #region Exception Types
1788 | 
1789 |     public class TomlFormatException : Exception
1790 |     {
1791 |         public TomlFormatException(string message) : base(message) { }
1792 |     }
1793 | 
1794 |     public class TomlParseException : Exception
1795 |     {
1796 |         public TomlParseException(TomlTable parsed, IEnumerable<TomlSyntaxException> exceptions) :
1797 |             base("TOML file contains format errors")
1798 |         {
1799 |             ParsedTable = parsed;
1800 |             SyntaxErrors = exceptions;
1801 |         }
1802 | 
1803 |         public TomlTable ParsedTable { get; }
1804 | 
1805 |         public IEnumerable<TomlSyntaxException> SyntaxErrors { get; }
1806 |     }
1807 | 
1808 |     public class TomlSyntaxException : Exception
1809 |     {
1810 |         public TomlSyntaxException(string message, TOMLParser.ParseState state, int line, int col) : base(message)
1811 |         {
1812 |             ParseState = state;
1813 |             Line = line;
1814 |             Column = col;
1815 |         }
1816 | 
1817 |         public TOMLParser.ParseState ParseState { get; }
1818 | 
1819 |         public int Line { get; }
1820 | 
1821 |         public int Column { get; }
1822 |     }
1823 | 
1824 |     #endregion
1825 | 
1826 |     #region Parse utilities
1827 | 
1828 |     internal static class TomlSyntax
1829 |     {
1830 |         #region Type Patterns
1831 | 
1832 |         public const string TRUE_VALUE = "true";
1833 |         public const string FALSE_VALUE = "false";
1834 |         public const string NAN_VALUE = "nan";
1835 |         public const string POS_NAN_VALUE = "+nan";
1836 |         public const string NEG_NAN_VALUE = "-nan";
1837 |         public const string INF_VALUE = "inf";
1838 |         public const string POS_INF_VALUE = "+inf";
1839 |         public const string NEG_INF_VALUE = "-inf";
1840 | 
1841 |         public static bool IsBoolean(string s) => s is TRUE_VALUE or FALSE_VALUE;
1842 | 
1843 |         public static bool IsPosInf(string s) => s is INF_VALUE or POS_INF_VALUE;
1844 | 
1845 |         public static bool IsNegInf(string s) => s == NEG_INF_VALUE;
1846 | 
1847 |         public static bool IsNaN(string s) => s is NAN_VALUE or POS_NAN_VALUE or NEG_NAN_VALUE;
1848 | 
1849 |         public static bool IsInteger(string s) => IntegerPattern.IsMatch(s);
1850 | 
1851 |         public static bool IsFloat(string s) => FloatPattern.IsMatch(s);
1852 | 
1853 |         public static bool IsIntegerWithBase(string s, out int numberBase)
1854 |         {
1855 |             numberBase = 10;
1856 |             var match = BasedIntegerPattern.Match(s);
1857 |             if (!match.Success) return false;
1858 |             IntegerBases.TryGetValue(match.Groups["base"].Value, out numberBase);
1859 |             return true;
1860 |         }
1861 | 
1862 |         /**
1863 |          * A pattern to verify the integer value according to the TOML specification.
1864 |          */
1865 |         public static readonly Regex IntegerPattern =
1866 |             new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)*)$", RegexOptions.Compiled);
1867 | 
1868 |         /**
1869 |          * A pattern to verify a special 0x, 0o and 0b forms of an integer according to the TOML specification.
1870 |          */
1871 |         public static readonly Regex BasedIntegerPattern =
1872 |             new(@"^0(?<base>x|b|o)(?!_)(_?[0-9A-F])*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
1873 | 
1874 |         /**
1875 |          * A pattern to verify the float value according to the TOML specification.
1876 |          */
1877 |         public static readonly Regex FloatPattern =
1878 |             new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)+)(((e(\+|-)?(?!_)(_?\d)+)?)|(\.(?!_)(_?\d)+(e(\+|-)?(?!_)(_?\d)+)?))$",
1879 |                 RegexOptions.Compiled | RegexOptions.IgnoreCase);
1880 | 
1881 |         /**
1882 |          * A helper dictionary to map TOML base codes into the radii.
1883 |          */
1884 |         public static readonly Dictionary<string, int> IntegerBases = new()
1885 |         {
1886 |             ["x"] = 16,
1887 |             ["o"] = 8,
1888 |             ["b"] = 2
1889 |         };
1890 | 
1891 |         /**
1892 |          * A helper dictionary to map non-decimal bases to their TOML identifiers
1893 |          */
1894 |         public static readonly Dictionary<int, string> BaseIdentifiers = new()
1895 |         {
1896 |             [2] = "b",
1897 |             [8] = "o",
1898 |             [16] = "x"
1899 |         };
1900 | 
1901 |         public const string RFC3339EmptySeparator = " ";
1902 |         public const string ISO861Separator = "T";
1903 |         public const string ISO861ZeroZone = "+00:00";
1904 |         public const string RFC3339ZeroZone = "Z";
1905 | 
1906 |         /**
1907 |          * Valid date formats with timezone as per RFC3339.
1908 |          */
1909 |         public static readonly string[] RFC3339Formats =
1910 |         {
1911 |             "yyyy'-'MM-ddTHH':'mm':'ssK", "yyyy'-'MM-ddTHH':'mm':'ss'.'fK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffK",
1912 |             "yyyy'-'MM-ddTHH':'mm':'ss'.'fffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffK",
1913 |             "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffffK",
1914 |             "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffffK"
1915 |         };
1916 | 
1917 |         /**
1918 |          * Valid date formats without timezone (assumes local) as per RFC3339.
1919 |          */
1920 |         public static readonly string[] RFC3339LocalDateTimeFormats =
1921 |         {
1922 |             "yyyy'-'MM-ddTHH':'mm':'ss", "yyyy'-'MM-ddTHH':'mm':'ss'.'f", "yyyy'-'MM-ddTHH':'mm':'ss'.'ff",
1923 |             "yyyy'-'MM-ddTHH':'mm':'ss'.'fff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffff",
1924 |             "yyyy'-'MM-ddTHH':'mm':'ss'.'fffff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffff",
1925 |             "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffff"
1926 |         };
1927 | 
1928 |         /**
1929 |          * Valid full date format as per TOML spec.
1930 |          */
1931 |         public static readonly string LocalDateFormat = "yyyy'-'MM'-'dd";
1932 | 
1933 |         /**
1934 |          * Valid time formats as per TOML spec.
1935 |          */
1936 |         public static readonly string[] RFC3339LocalTimeFormats =
1937 |         {
1938 |             "HH':'mm':'ss", "HH':'mm':'ss'.'f", "HH':'mm':'ss'.'ff", "HH':'mm':'ss'.'fff", "HH':'mm':'ss'.'ffff",
1939 |             "HH':'mm':'ss'.'fffff", "HH':'mm':'ss'.'ffffff", "HH':'mm':'ss'.'fffffff"
1940 |         };
1941 | 
1942 |         #endregion
1943 | 
1944 |         #region Character definitions
1945 | 
1946 |         public const char ARRAY_END_SYMBOL = ']';
1947 |         public const char ITEM_SEPARATOR = ',';
1948 |         public const char ARRAY_START_SYMBOL = '[';
1949 |         public const char BASIC_STRING_SYMBOL = '\"';
1950 |         public const char COMMENT_SYMBOL = '#';
1951 |         public const char ESCAPE_SYMBOL = '\\';
1952 |         public const char KEY_VALUE_SEPARATOR = '=';
1953 |         public const char NEWLINE_CARRIAGE_RETURN_CHARACTER = '\r';
1954 |         public const char NEWLINE_CHARACTER = '\n';
1955 |         public const char SUBKEY_SEPARATOR = '.';
1956 |         public const char TABLE_END_SYMBOL = ']';
1957 |         public const char TABLE_START_SYMBOL = '[';
1958 |         public const char INLINE_TABLE_START_SYMBOL = '{';
1959 |         public const char INLINE_TABLE_END_SYMBOL = '}';
1960 |         public const char LITERAL_STRING_SYMBOL = '\'';
1961 |         public const char INT_NUMBER_SEPARATOR = '_';
1962 | 
1963 |         public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER };
1964 | 
1965 |         public static bool IsQuoted(char c) => c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL;
1966 | 
1967 |         public static bool IsWhiteSpace(char c) => c is ' ' or '\t';
1968 | 
1969 |         public static bool IsNewLine(char c) => c is NEWLINE_CHARACTER or NEWLINE_CARRIAGE_RETURN_CHARACTER;
1970 | 
1971 |         public static bool IsLineBreak(char c) => c == NEWLINE_CHARACTER;
1972 | 
1973 |         public static bool IsEmptySpace(char c) => IsWhiteSpace(c) || IsNewLine(c);
1974 | 
1975 |         public static bool IsBareKey(char c) =>
1976 |             c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_' or '-';
1977 | 
1978 |         public static bool MustBeEscaped(char c, bool allowNewLines = false)
1979 |         {
1980 |             var result = c is (>= '\u0000' and <= '\u0008') or '\u000b' or '\u000c' or (>= '\u000e' and <= '\u001f') or '\u007f';
1981 |             if (!allowNewLines)
1982 |                 result |= c is >= '\u000a' and <= '\u000e';
1983 |             return result;
1984 |         }
1985 | 
1986 |         public static bool IsValueSeparator(char c) =>
1987 |             c is ITEM_SEPARATOR or ARRAY_END_SYMBOL or INLINE_TABLE_END_SYMBOL;
1988 | 
1989 |         #endregion
1990 |     }
1991 | 
1992 |     internal static class StringUtils
1993 |     {
1994 |         public static string AsKey(this string key)
1995 |         {
1996 |             var quote = key == string.Empty || key.Any(c => !TomlSyntax.IsBareKey(c));
1997 |             return !quote ? key : $"{TomlSyntax.BASIC_STRING_SYMBOL}{key.Escape()}{TomlSyntax.BASIC_STRING_SYMBOL}";
1998 |         }
1999 | 
2000 |         public static string Join(this string self, IEnumerable<string> subItems)
2001 |         {
2002 |             var sb = new StringBuilder();
2003 |             var first = true;
2004 | 
2005 |             foreach (var subItem in subItems)
2006 |             {
2007 |                 if (!first) sb.Append(self);
2008 |                 first = false;
2009 |                 sb.Append(subItem);
2010 |             }
2011 | 
2012 |             return sb.ToString();
2013 |         }
2014 | 
2015 |         public delegate bool TryDateParseDelegate<T>(string s, string format, IFormatProvider ci, DateTimeStyles dts, out T dt);
2016 | 
2017 |         public static bool TryParseDateTime<T>(string s,
2018 |                                                string[] formats,
2019 |                                                DateTimeStyles styles,
2020 |                                                TryDateParseDelegate<T> parser,
2021 |                                                out T dateTime,
2022 |                                                out int parsedFormat)
2023 |         {
2024 |             parsedFormat = 0;
2025 |             dateTime = default;
2026 |             for (var i = 0; i < formats.Length; i++)
2027 |             {
2028 |                 var format = formats[i];
2029 |                 if (!parser(s, format, CultureInfo.InvariantCulture, styles, out dateTime)) continue;
2030 |                 parsedFormat = i;
2031 |                 return true;
2032 |             }
2033 | 
2034 |             return false;
2035 |         }
2036 | 
2037 |         public static void AsComment(this string self, TextWriter tw)
2038 |         {
2039 |             foreach (var line in self.Split(TomlSyntax.NEWLINE_CHARACTER))
2040 |                 tw.WriteLine($"{TomlSyntax.COMMENT_SYMBOL} {line.Trim()}");
2041 |         }
2042 | 
2043 |         public static string RemoveAll(this string txt, char toRemove)
2044 |         {
2045 |             var sb = new StringBuilder(txt.Length);
2046 |             foreach (var c in txt.Where(c => c != toRemove))
2047 |                 sb.Append(c);
2048 |             return sb.ToString();
2049 |         }
2050 | 
2051 |         public static string Escape(this string txt, bool escapeNewlines = true)
2052 |         {
2053 |             var stringBuilder = new StringBuilder(txt.Length + 2);
2054 |             for (var i = 0; i < txt.Length; i++)
2055 |             {
2056 |                 var c = txt[i];
2057 | 
2058 |                 static string CodePoint(string txt, ref int i, char c) => char.IsSurrogatePair(txt, i)
2059 |                     ? $"\\U{char.ConvertToUtf32(txt, i++):X8}"
2060 |                     : $"\\u{(ushort)c:X4}";
2061 | 
2062 |                 stringBuilder.Append(c switch
2063 |                 {
2064 |                     '\b' => @"\b",
2065 |                     '\t' => @"\t",
2066 |                     '\n' when escapeNewlines => @"\n",
2067 |                     '\f' => @"\f",
2068 |                     '\r' when escapeNewlines => @"\r",
2069 |                     '\\' => @"\\",
2070 |                     '\"' => @"\""",
2071 |                     var _ when TomlSyntax.MustBeEscaped(c, !escapeNewlines) || TOML.ForceASCII && c > sbyte.MaxValue =>
2072 |                         CodePoint(txt, ref i, c),
2073 |                     var _ => c
2074 |                 });
2075 |             }
2076 | 
2077 |             return stringBuilder.ToString();
2078 |         }
2079 | 
2080 |         public static bool TryUnescape(this string txt, out string unescaped, out Exception exception)
2081 |         {
2082 |             try
2083 |             {
2084 |                 exception = null;
2085 |                 unescaped = txt.Unescape();
2086 |                 return true;
2087 |             }
2088 |             catch (Exception e)
2089 |             {
2090 |                 exception = e;
2091 |                 unescaped = null;
2092 |                 return false;
2093 |             }
2094 |         }
2095 | 
2096 |         public static string Unescape(this string txt)
2097 |         {
2098 |             if (string.IsNullOrEmpty(txt)) return txt;
2099 |             var stringBuilder = new StringBuilder(txt.Length);
2100 |             for (var i = 0; i < txt.Length;)
2101 |             {
2102 |                 var num = txt.IndexOf('\\', i);
2103 |                 var next = num + 1;
2104 |                 if (num < 0 || num == txt.Length - 1) num = txt.Length;
2105 |                 stringBuilder.Append(txt, i, num - i);
2106 |                 if (num >= txt.Length) break;
2107 |                 var c = txt[next];
2108 | 
2109 |                 static string CodePoint(int next, string txt, ref int num, int size)
2110 |                 {
2111 |                     if (next + size >= txt.Length) throw new Exception("Undefined escape sequence!");
2112 |                     num += size;
2113 |                     return char.ConvertFromUtf32(Convert.ToInt32(txt.Substring(next + 1, size), 16));
2114 |                 }
2115 | 
2116 |                 stringBuilder.Append(c switch
2117 |                 {
2118 |                     'b' => "\b",
2119 |                     't' => "\t",
2120 |                     'n' => "\n",
2121 |                     'f' => "\f",
2122 |                     'r' => "\r",
2123 |                     '\'' => "\'",
2124 |                     '\"' => "\"",
2125 |                     '\\' => "\\",
2126 |                     'u' => CodePoint(next, txt, ref num, 4),
2127 |                     'U' => CodePoint(next, txt, ref num, 8),
2128 |                     var _ => throw new Exception("Undefined escape sequence!")
2129 |                 });
2130 |                 i = num + 2;
2131 |             }
2132 | 
2133 |             return stringBuilder.ToString();
2134 |         }
2135 |     }
2136 | 
2137 |     #endregion
2138 | }
2139 | 
```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs:
--------------------------------------------------------------------------------

```csharp
   1 | using System;
   2 | using System.Collections.Generic;
   3 | using System.Diagnostics;
   4 | using System.Security.Cryptography;
   5 | using System.Text;
   6 | using System.Net.Sockets;
   7 | using System.Net;
   8 | using System.IO;
   9 | using System.Linq;
  10 | using System.Runtime.InteropServices;
  11 | using Newtonsoft.Json;
  12 | using Newtonsoft.Json.Linq;
  13 | using UnityEditor;
  14 | using UnityEngine;
  15 | using MCPForUnity.Editor.Data;
  16 | using MCPForUnity.Editor.Helpers;
  17 | using MCPForUnity.Editor.Models;
  18 | 
  19 | namespace MCPForUnity.Editor.Windows
  20 | {
  21 |     public class MCPForUnityEditorWindow : EditorWindow
  22 |     {
  23 |         private bool isUnityBridgeRunning = false;
  24 |         private Vector2 scrollPosition;
  25 |         private string pythonServerInstallationStatus = "Not Installed";
  26 |         private Color pythonServerInstallationStatusColor = Color.red;
  27 |         private const int mcpPort = 6500; // MCP port (still hardcoded for MCP server)
  28 |         private readonly McpClients mcpClients = new();
  29 |         private bool autoRegisterEnabled;
  30 |         private bool lastClientRegisteredOk;
  31 |         private bool lastBridgeVerifiedOk;
  32 |         private string pythonDirOverride = null;
  33 |         private bool debugLogsEnabled;
  34 | 
  35 |         // Script validation settings
  36 |         private int validationLevelIndex = 1; // Default to Standard
  37 |         private readonly string[] validationLevelOptions = new string[]
  38 |         {
  39 |             "Basic - Only syntax checks",
  40 |             "Standard - Syntax + Unity practices",
  41 |             "Comprehensive - All checks + semantic analysis",
  42 |             "Strict - Full semantic validation (requires Roslyn)"
  43 |         };
  44 | 
  45 |         // UI state
  46 |         private int selectedClientIndex = 0;
  47 | 
  48 |         [MenuItem("Window/MCP For Unity")]
  49 |         public static void ShowWindow()
  50 |         {
  51 |             GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
  52 |         }
  53 | 
  54 |         private void OnEnable()
  55 |         {
  56 |             UpdatePythonServerInstallationStatus();
  57 | 
  58 |             // Refresh bridge status
  59 |             isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
  60 |             autoRegisterEnabled = EditorPrefs.GetBool("MCPForUnity.AutoRegisterEnabled", true);
  61 |             debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
  62 |             if (debugLogsEnabled)
  63 |             {
  64 |                 LogDebugPrefsState();
  65 |             }
  66 |             foreach (McpClient mcpClient in mcpClients.clients)
  67 |             {
  68 |                 CheckMcpConfiguration(mcpClient);
  69 |             }
  70 | 
  71 |             // Load validation level setting
  72 |             LoadValidationLevelSetting();
  73 | 
  74 |             // Show one-time migration dialog
  75 |             ShowMigrationDialogIfNeeded();
  76 | 
  77 |             // First-run auto-setup only if Claude CLI is available
  78 |             if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
  79 |             {
  80 |                 AutoFirstRunSetup();
  81 |             }
  82 |         }
  83 | 
  84 |         private void OnFocus()
  85 |         {
  86 |             // Refresh bridge running state on focus in case initialization completed after domain reload
  87 |             isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
  88 |             if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
  89 |             {
  90 |                 McpClient selectedClient = mcpClients.clients[selectedClientIndex];
  91 |                 CheckMcpConfiguration(selectedClient);
  92 |             }
  93 |             Repaint();
  94 |         }
  95 | 
  96 |         private Color GetStatusColor(McpStatus status)
  97 |         {
  98 |             // Return appropriate color based on the status enum
  99 |             return status switch
 100 |             {
 101 |                 McpStatus.Configured => Color.green,
 102 |                 McpStatus.Running => Color.green,
 103 |                 McpStatus.Connected => Color.green,
 104 |                 McpStatus.IncorrectPath => Color.yellow,
 105 |                 McpStatus.CommunicationError => Color.yellow,
 106 |                 McpStatus.NoResponse => Color.yellow,
 107 |                 _ => Color.red, // Default to red for error states or not configured
 108 |             };
 109 |         }
 110 | 
 111 |         private void UpdatePythonServerInstallationStatus()
 112 |         {
 113 |             try
 114 |             {
 115 |                 string installedPath = ServerInstaller.GetServerPath();
 116 |                 bool installedOk = !string.IsNullOrEmpty(installedPath) && File.Exists(Path.Combine(installedPath, "server.py"));
 117 |                 if (installedOk)
 118 |                 {
 119 |                     pythonServerInstallationStatus = "Installed";
 120 |                     pythonServerInstallationStatusColor = Color.green;
 121 |                     return;
 122 |                 }
 123 | 
 124 |                 // Fall back to embedded/dev source via our existing resolution logic
 125 |                 string embeddedPath = FindPackagePythonDirectory();
 126 |                 bool embeddedOk = !string.IsNullOrEmpty(embeddedPath) && File.Exists(Path.Combine(embeddedPath, "server.py"));
 127 |                 if (embeddedOk)
 128 |                 {
 129 |                     pythonServerInstallationStatus = "Installed (Embedded)";
 130 |                     pythonServerInstallationStatusColor = Color.green;
 131 |                 }
 132 |                 else
 133 |                 {
 134 |                     pythonServerInstallationStatus = "Not Installed";
 135 |                     pythonServerInstallationStatusColor = Color.red;
 136 |                 }
 137 |             }
 138 |             catch
 139 |             {
 140 |                 pythonServerInstallationStatus = "Not Installed";
 141 |                 pythonServerInstallationStatusColor = Color.red;
 142 |             }
 143 |         }
 144 | 
 145 | 
 146 |         private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12)
 147 |         {
 148 |             float offsetX = (statusRect.width - size) / 2;
 149 |             float offsetY = (statusRect.height - size) / 2;
 150 |             Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size);
 151 |             Vector3 center = new(
 152 |                 dotRect.x + (dotRect.width / 2),
 153 |                 dotRect.y + (dotRect.height / 2),
 154 |                 0
 155 |             );
 156 |             float radius = size / 2;
 157 | 
 158 |             // Draw the main dot
 159 |             Handles.color = statusColor;
 160 |             Handles.DrawSolidDisc(center, Vector3.forward, radius);
 161 | 
 162 |             // Draw the border
 163 |             Color borderColor = new(
 164 |                 statusColor.r * 0.7f,
 165 |                 statusColor.g * 0.7f,
 166 |                 statusColor.b * 0.7f
 167 |             );
 168 |             Handles.color = borderColor;
 169 |             Handles.DrawWireDisc(center, Vector3.forward, radius);
 170 |         }
 171 | 
 172 |         private void OnGUI()
 173 |         {
 174 |             scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
 175 | 
 176 |             // Migration warning banner (non-dismissible)
 177 |             DrawMigrationWarningBanner();
 178 | 
 179 |             // Header
 180 |             DrawHeader();
 181 | 
 182 |             // Compute equal column widths for uniform layout
 183 |             float horizontalSpacing = 2f;
 184 |             float outerPadding = 20f; // approximate padding
 185 |             // Make columns a bit less wide for a tighter layout
 186 |             float computed = (position.width - outerPadding - horizontalSpacing) / 2f;
 187 |             float colWidth = Mathf.Clamp(computed, 220f, 340f);
 188 |             // Use fixed heights per row so paired panels match exactly
 189 |             float topPanelHeight = 190f;
 190 |             float bottomPanelHeight = 230f;
 191 | 
 192 |             // Top row: Server Status (left) and Unity Bridge (right)
 193 |             EditorGUILayout.BeginHorizontal();
 194 |             {
 195 |                 EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight));
 196 |                 DrawServerStatusSection();
 197 |                 EditorGUILayout.EndVertical();
 198 | 
 199 |                 EditorGUILayout.Space(horizontalSpacing);
 200 | 
 201 |                 EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight));
 202 |                 DrawBridgeSection();
 203 |                 EditorGUILayout.EndVertical();
 204 |             }
 205 |             EditorGUILayout.EndHorizontal();
 206 | 
 207 |             EditorGUILayout.Space(10);
 208 | 
 209 |             // Second row: MCP Client Configuration (left) and Script Validation (right)
 210 |             EditorGUILayout.BeginHorizontal();
 211 |             {
 212 |                 EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight));
 213 |                 DrawUnifiedClientConfiguration();
 214 |                 EditorGUILayout.EndVertical();
 215 | 
 216 |                 EditorGUILayout.Space(horizontalSpacing);
 217 | 
 218 |                 EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight));
 219 |                 DrawValidationSection();
 220 |                 EditorGUILayout.EndVertical();
 221 |             }
 222 |             EditorGUILayout.EndHorizontal();
 223 | 
 224 |             // Minimal bottom padding
 225 |             EditorGUILayout.Space(2);
 226 | 
 227 |             EditorGUILayout.EndScrollView();
 228 |         }
 229 | 
 230 |         private void DrawHeader()
 231 |         {
 232 |             EditorGUILayout.Space(15);
 233 |             Rect titleRect = EditorGUILayout.GetControlRect(false, 40);
 234 |             EditorGUI.DrawRect(titleRect, new Color(0.2f, 0.2f, 0.2f, 0.1f));
 235 | 
 236 |             GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel)
 237 |             {
 238 |                 fontSize = 16,
 239 |                 alignment = TextAnchor.MiddleLeft
 240 |             };
 241 | 
 242 |             GUI.Label(
 243 |                 new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height),
 244 |                 "MCP For Unity",
 245 |                 titleStyle
 246 |             );
 247 | 
 248 |             // Place the Show Debug Logs toggle on the same header row, right-aligned
 249 |             float toggleWidth = 160f;
 250 |             Rect toggleRect = new Rect(titleRect.xMax - toggleWidth - 12f, titleRect.y + 10f, toggleWidth, 20f);
 251 |             bool newDebug = GUI.Toggle(toggleRect, debugLogsEnabled, "Show Debug Logs");
 252 |             if (newDebug != debugLogsEnabled)
 253 |             {
 254 |                 debugLogsEnabled = newDebug;
 255 |                 EditorPrefs.SetBool("MCPForUnity.DebugLogs", debugLogsEnabled);
 256 |                 if (debugLogsEnabled)
 257 |                 {
 258 |                     LogDebugPrefsState();
 259 |                 }
 260 |             }
 261 |             EditorGUILayout.Space(15);
 262 |         }
 263 | 
 264 |         private void LogDebugPrefsState()
 265 |         {
 266 |             try
 267 |             {
 268 |                 string pythonDirOverridePref = SafeGetPrefString("MCPForUnity.PythonDirOverride");
 269 |                 string uvPathPref = SafeGetPrefString("MCPForUnity.UvPath");
 270 |                 string serverSrcPref = SafeGetPrefString("MCPForUnity.ServerSrc");
 271 |                 bool useEmbedded = SafeGetPrefBool("MCPForUnity.UseEmbeddedServer");
 272 | 
 273 |                 // Version-scoped detection key
 274 |                 string embeddedVer = ReadEmbeddedVersionOrFallback();
 275 |                 string detectKey = $"MCPForUnity.LegacyDetectLogged:{embeddedVer}";
 276 |                 bool detectLogged = SafeGetPrefBool(detectKey);
 277 | 
 278 |                 // Project-scoped auto-register key
 279 |                 string projectPath = Application.dataPath ?? string.Empty;
 280 |                 string autoKey = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}";
 281 |                 bool autoRegistered = SafeGetPrefBool(autoKey);
 282 | 
 283 |                 MCPForUnity.Editor.Helpers.McpLog.Info(
 284 |                     "MCP Debug Prefs:\n" +
 285 |                     $"  DebugLogs: {debugLogsEnabled}\n" +
 286 |                     $"  PythonDirOverride: '{pythonDirOverridePref}'\n" +
 287 |                     $"  UvPath: '{uvPathPref}'\n" +
 288 |                     $"  ServerSrc: '{serverSrcPref}'\n" +
 289 |                     $"  UseEmbeddedServer: {useEmbedded}\n" +
 290 |                     $"  DetectOnceKey: '{detectKey}' => {detectLogged}\n" +
 291 |                     $"  AutoRegisteredKey: '{autoKey}' => {autoRegistered}",
 292 |                     always: false
 293 |                 );
 294 |             }
 295 |             catch (Exception ex)
 296 |             {
 297 |                 UnityEngine.Debug.LogWarning($"MCP Debug Prefs logging failed: {ex.Message}");
 298 |             }
 299 |         }
 300 | 
 301 |         private static string SafeGetPrefString(string key)
 302 |         {
 303 |             try { return EditorPrefs.GetString(key, string.Empty) ?? string.Empty; } catch { return string.Empty; }
 304 |         }
 305 | 
 306 |         private static bool SafeGetPrefBool(string key)
 307 |         {
 308 |             try { return EditorPrefs.GetBool(key, false); } catch { return false; }
 309 |         }
 310 | 
 311 |         private static string ReadEmbeddedVersionOrFallback()
 312 |         {
 313 |             try
 314 |             {
 315 |                 if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
 316 |                 {
 317 |                     var p = Path.Combine(embeddedSrc, "server_version.txt");
 318 |                     if (File.Exists(p))
 319 |                     {
 320 |                         var s = File.ReadAllText(p)?.Trim();
 321 |                         if (!string.IsNullOrEmpty(s)) return s;
 322 |                     }
 323 |                 }
 324 |             }
 325 |             catch { }
 326 |             return "unknown";
 327 |         }
 328 | 
 329 |         private void DrawServerStatusSection()
 330 |         {
 331 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
 332 | 
 333 |             GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
 334 |             {
 335 |                 fontSize = 14
 336 |             };
 337 |             EditorGUILayout.LabelField("Server Status", sectionTitleStyle);
 338 |             EditorGUILayout.Space(8);
 339 | 
 340 |             EditorGUILayout.BeginHorizontal();
 341 |             Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
 342 |             DrawStatusDot(statusRect, pythonServerInstallationStatusColor, 16);
 343 | 
 344 |             GUIStyle statusStyle = new GUIStyle(EditorStyles.label)
 345 |             {
 346 |                 fontSize = 12,
 347 |                 fontStyle = FontStyle.Bold
 348 |             };
 349 |             EditorGUILayout.LabelField(pythonServerInstallationStatus, statusStyle, GUILayout.Height(28));
 350 |             EditorGUILayout.EndHorizontal();
 351 | 
 352 |             EditorGUILayout.Space(5);
 353 | 
 354 |             EditorGUILayout.BeginHorizontal();
 355 |             bool isAutoMode = MCPForUnityBridge.IsAutoConnectMode();
 356 |             GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 };
 357 |             EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle);
 358 |             GUILayout.FlexibleSpace();
 359 |             EditorGUILayout.EndHorizontal();
 360 | 
 361 |             int currentUnityPort = MCPForUnityBridge.GetCurrentPort();
 362 |             GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel)
 363 |             {
 364 |                 fontSize = 11
 365 |             };
 366 |             EditorGUILayout.LabelField($"Ports: Unity {currentUnityPort}, MCP {mcpPort}", portStyle);
 367 |             EditorGUILayout.Space(5);
 368 | 
 369 |             /// Auto-Setup button below ports
 370 |             string setupButtonText = (lastClientRegisteredOk && lastBridgeVerifiedOk) ? "Connected ✓" : "Auto-Setup";
 371 |             if (GUILayout.Button(setupButtonText, GUILayout.Height(24)))
 372 |             {
 373 |                 RunSetupNow();
 374 |             }
 375 |             EditorGUILayout.Space(4);
 376 | 
 377 |             // Rebuild MCP Server button with tooltip tag
 378 |             using (new EditorGUILayout.HorizontalScope())
 379 |             {
 380 |                 GUILayout.FlexibleSpace();
 381 |                 GUIContent repairLabel = new GUIContent(
 382 |                     "Rebuild MCP Server",
 383 |                     "Deletes the installed server and re-copies it from the package. Use this to update the server after making source code changes or if the installation is corrupted."
 384 |                 );
 385 |                 if (GUILayout.Button(repairLabel, GUILayout.Width(160), GUILayout.Height(22)))
 386 |                 {
 387 |                     bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RebuildMcpServer();
 388 |                     if (ok)
 389 |                     {
 390 |                         EditorUtility.DisplayDialog("MCP For Unity", "Server rebuilt successfully.", "OK");
 391 |                         UpdatePythonServerInstallationStatus();
 392 |                     }
 393 |                     else
 394 |                     {
 395 |                         EditorUtility.DisplayDialog("MCP For Unity", "Rebuild failed. Please check Console for details.", "OK");
 396 |                     }
 397 |                 }
 398 |             }
 399 |             // (Removed descriptive tool tag under the Repair button)
 400 | 
 401 |             // (Show Debug Logs toggle moved to header)
 402 |             EditorGUILayout.Space(2);
 403 | 
 404 |             // Python detection warning with link
 405 |             if (!IsPythonDetected())
 406 |             {
 407 |                 GUIStyle warnStyle = new GUIStyle(EditorStyles.label) { richText = true, wordWrap = true };
 408 |                 EditorGUILayout.LabelField("<color=#cc3333><b>Warning:</b></color> No Python installation found.", warnStyle);
 409 |                 using (new EditorGUILayout.HorizontalScope())
 410 |                 {
 411 |                     if (GUILayout.Button("Open Install Instructions", GUILayout.Width(200)))
 412 |                     {
 413 |                         Application.OpenURL("https://www.python.org/downloads/");
 414 |                     }
 415 |                 }
 416 |                 EditorGUILayout.Space(4);
 417 |             }
 418 | 
 419 |             // Troubleshooting helpers
 420 |             if (pythonServerInstallationStatusColor != Color.green)
 421 |             {
 422 |                 using (new EditorGUILayout.HorizontalScope())
 423 |                 {
 424 |                     if (GUILayout.Button("Select server folder…", GUILayout.Width(160)))
 425 |                     {
 426 |                         string picked = EditorUtility.OpenFolderPanel("Select UnityMcpServer/src", Application.dataPath, "");
 427 |                         if (!string.IsNullOrEmpty(picked) && File.Exists(Path.Combine(picked, "server.py")))
 428 |                         {
 429 |                             pythonDirOverride = picked;
 430 |                             EditorPrefs.SetString("MCPForUnity.PythonDirOverride", pythonDirOverride);
 431 |                             UpdatePythonServerInstallationStatus();
 432 |                         }
 433 |                         else if (!string.IsNullOrEmpty(picked))
 434 |                         {
 435 |                             EditorUtility.DisplayDialog("Invalid Selection", "The selected folder does not contain server.py", "OK");
 436 |                         }
 437 |                     }
 438 |                     if (GUILayout.Button("Verify again", GUILayout.Width(120)))
 439 |                     {
 440 |                         UpdatePythonServerInstallationStatus();
 441 |                     }
 442 |                 }
 443 |             }
 444 |             EditorGUILayout.EndVertical();
 445 |         }
 446 | 
 447 |         private void DrawBridgeSection()
 448 |         {
 449 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
 450 | 
 451 |             // Always reflect the live state each repaint to avoid stale UI after recompiles
 452 |             isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
 453 | 
 454 |             GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
 455 |             {
 456 |                 fontSize = 14
 457 |             };
 458 |             EditorGUILayout.LabelField("Unity Bridge", sectionTitleStyle);
 459 |             EditorGUILayout.Space(8);
 460 | 
 461 |             EditorGUILayout.BeginHorizontal();
 462 |             Color bridgeColor = isUnityBridgeRunning ? Color.green : Color.red;
 463 |             Rect bridgeStatusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
 464 |             DrawStatusDot(bridgeStatusRect, bridgeColor, 16);
 465 | 
 466 |             GUIStyle bridgeStatusStyle = new GUIStyle(EditorStyles.label)
 467 |             {
 468 |                 fontSize = 12,
 469 |                 fontStyle = FontStyle.Bold
 470 |             };
 471 |             EditorGUILayout.LabelField(isUnityBridgeRunning ? "Running" : "Stopped", bridgeStatusStyle, GUILayout.Height(28));
 472 |             EditorGUILayout.EndHorizontal();
 473 | 
 474 |             EditorGUILayout.Space(8);
 475 |             if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge", GUILayout.Height(32)))
 476 |             {
 477 |                 ToggleUnityBridge();
 478 |             }
 479 |             EditorGUILayout.Space(5);
 480 |             EditorGUILayout.EndVertical();
 481 |         }
 482 | 
 483 |         private void DrawValidationSection()
 484 |         {
 485 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
 486 | 
 487 |             GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
 488 |             {
 489 |                 fontSize = 14
 490 |             };
 491 |             EditorGUILayout.LabelField("Script Validation", sectionTitleStyle);
 492 |             EditorGUILayout.Space(8);
 493 | 
 494 |             EditorGUI.BeginChangeCheck();
 495 |             validationLevelIndex = EditorGUILayout.Popup("Validation Level", validationLevelIndex, validationLevelOptions, GUILayout.Height(20));
 496 |             if (EditorGUI.EndChangeCheck())
 497 |             {
 498 |                 SaveValidationLevelSetting();
 499 |             }
 500 | 
 501 |             EditorGUILayout.Space(8);
 502 |             string description = GetValidationLevelDescription(validationLevelIndex);
 503 |             EditorGUILayout.HelpBox(description, MessageType.Info);
 504 |             EditorGUILayout.Space(4);
 505 |             // (Show Debug Logs toggle moved to header)
 506 |             EditorGUILayout.Space(2);
 507 |             EditorGUILayout.EndVertical();
 508 |         }
 509 | 
 510 |         private void DrawUnifiedClientConfiguration()
 511 |         {
 512 |             EditorGUILayout.BeginVertical(EditorStyles.helpBox);
 513 | 
 514 |             GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
 515 |             {
 516 |                 fontSize = 14
 517 |             };
 518 |             EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle);
 519 |             EditorGUILayout.Space(10);
 520 | 
 521 |             // (Auto-connect toggle removed per design)
 522 | 
 523 |             // Client selector
 524 |             string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray();
 525 |             EditorGUI.BeginChangeCheck();
 526 |             selectedClientIndex = EditorGUILayout.Popup("Select Client", selectedClientIndex, clientNames, GUILayout.Height(20));
 527 |             if (EditorGUI.EndChangeCheck())
 528 |             {
 529 |                 selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1);
 530 |             }
 531 | 
 532 |             EditorGUILayout.Space(10);
 533 | 
 534 |             if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
 535 |             {
 536 |                 McpClient selectedClient = mcpClients.clients[selectedClientIndex];
 537 |                 DrawClientConfigurationCompact(selectedClient);
 538 |             }
 539 | 
 540 |             EditorGUILayout.Space(5);
 541 |             EditorGUILayout.EndVertical();
 542 |         }
 543 | 
 544 |         private void AutoFirstRunSetup()
 545 |         {
 546 |             try
 547 |             {
 548 |                 // Project-scoped one-time flag
 549 |                 string projectPath = Application.dataPath ?? string.Empty;
 550 |                 string key = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}";
 551 |                 if (EditorPrefs.GetBool(key, false))
 552 |                 {
 553 |                     return;
 554 |                 }
 555 | 
 556 |                 // Attempt client registration using discovered Python server dir
 557 |                 pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null);
 558 |                 string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
 559 |                 if (!string.IsNullOrEmpty(pythonDir) && File.Exists(Path.Combine(pythonDir, "server.py")))
 560 |                 {
 561 |                     bool anyRegistered = false;
 562 |                     foreach (McpClient client in mcpClients.clients)
 563 |                     {
 564 |                         try
 565 |                         {
 566 |                             if (client.mcpType == McpTypes.ClaudeCode)
 567 |                             {
 568 |                                 // Only attempt if Claude CLI is present
 569 |                                 if (!IsClaudeConfigured() && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
 570 |                                 {
 571 |                                     RegisterWithClaudeCode(pythonDir);
 572 |                                     anyRegistered = true;
 573 |                                 }
 574 |                             }
 575 |                             else
 576 |                             {
 577 |                                 CheckMcpConfiguration(client);
 578 |                                 bool alreadyConfigured = client.status == McpStatus.Configured;
 579 |                                 if (!alreadyConfigured)
 580 |                                 {
 581 |                                     ConfigureMcpClient(client);
 582 |                                     anyRegistered = true;
 583 |                                 }
 584 |                             }
 585 |                         }
 586 |                         catch (Exception ex)
 587 |                         {
 588 |                             MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}");
 589 |                         }
 590 |                     }
 591 |                     lastClientRegisteredOk = anyRegistered
 592 |                         || IsCursorConfigured(pythonDir)
 593 |                         || CodexConfigHelper.IsCodexConfigured(pythonDir)
 594 |                         || IsClaudeConfigured();
 595 |                 }
 596 | 
 597 |                 // Ensure the bridge is listening and has a fresh saved port
 598 |                 if (!MCPForUnityBridge.IsRunning)
 599 |                 {
 600 |                     try
 601 |                     {
 602 |                         MCPForUnityBridge.StartAutoConnect();
 603 |                         isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
 604 |                         Repaint();
 605 |                     }
 606 |                     catch (Exception ex)
 607 |                     {
 608 |                         MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup StartAutoConnect failed: {ex.Message}");
 609 |                     }
 610 |                 }
 611 | 
 612 |                 // Verify bridge with a quick ping
 613 |                 lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort());
 614 | 
 615 |                 EditorPrefs.SetBool(key, true);
 616 |             }
 617 |             catch (Exception e)
 618 |             {
 619 |                 MCPForUnity.Editor.Helpers.McpLog.Warn($"MCP for Unity auto-setup skipped: {e.Message}");
 620 |             }
 621 |         }
 622 | 
 623 |         private static string ComputeSha1(string input)
 624 |         {
 625 |             try
 626 |             {
 627 |                 using SHA1 sha1 = SHA1.Create();
 628 |                 byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
 629 |                 byte[] hash = sha1.ComputeHash(bytes);
 630 |                 StringBuilder sb = new StringBuilder(hash.Length * 2);
 631 |                 foreach (byte b in hash)
 632 |                 {
 633 |                     sb.Append(b.ToString("x2"));
 634 |                 }
 635 |                 return sb.ToString();
 636 |             }
 637 |             catch
 638 |             {
 639 |                 return "";
 640 |             }
 641 |         }
 642 | 
 643 |         private void RunSetupNow()
 644 |         {
 645 |             // Force a one-shot setup regardless of first-run flag
 646 |             try
 647 |             {
 648 |                 pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null);
 649 |                 string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
 650 |                 if (string.IsNullOrEmpty(pythonDir) || !File.Exists(Path.Combine(pythonDir, "server.py")))
 651 |                 {
 652 |                     EditorUtility.DisplayDialog("Setup", "Python server not found. Please select UnityMcpServer/src.", "OK");
 653 |                     return;
 654 |                 }
 655 | 
 656 |                 bool anyRegistered = false;
 657 |                 foreach (McpClient client in mcpClients.clients)
 658 |                 {
 659 |                     try
 660 |                     {
 661 |                         if (client.mcpType == McpTypes.ClaudeCode)
 662 |                         {
 663 |                             if (!IsClaudeConfigured())
 664 |                             {
 665 |                                 RegisterWithClaudeCode(pythonDir);
 666 |                                 anyRegistered = true;
 667 |                             }
 668 |                         }
 669 |                         else
 670 |                         {
 671 |                             CheckMcpConfiguration(client);
 672 |                             bool alreadyConfigured = client.status == McpStatus.Configured;
 673 |                             if (!alreadyConfigured)
 674 |                             {
 675 |                                 ConfigureMcpClient(client);
 676 |                                 anyRegistered = true;
 677 |                             }
 678 |                         }
 679 |                     }
 680 |                     catch (Exception ex)
 681 |                     {
 682 |                         UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}");
 683 |                     }
 684 |                 }
 685 |                 lastClientRegisteredOk = anyRegistered
 686 |                     || IsCursorConfigured(pythonDir)
 687 |                     || CodexConfigHelper.IsCodexConfigured(pythonDir)
 688 |                     || IsClaudeConfigured();
 689 | 
 690 |                 // Restart/ensure bridge
 691 |                 MCPForUnityBridge.StartAutoConnect();
 692 |                 isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
 693 | 
 694 |                 // Verify
 695 |                 lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort());
 696 |                 Repaint();
 697 |             }
 698 |             catch (Exception e)
 699 |             {
 700 |                 EditorUtility.DisplayDialog("Setup Failed", e.Message, "OK");
 701 |             }
 702 |         }
 703 | 
 704 |         private static bool IsCursorConfigured(string pythonDir)
 705 |         {
 706 |             try
 707 |             {
 708 |                 string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
 709 |                     ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 710 |                         ".cursor", "mcp.json")
 711 |                     : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
 712 |                         ".cursor", "mcp.json");
 713 |                 if (!File.Exists(configPath)) return false;
 714 |                 string json = File.ReadAllText(configPath);
 715 |                 dynamic cfg = JsonConvert.DeserializeObject(json);
 716 |                 var servers = cfg?.mcpServers;
 717 |                 if (servers == null) return false;
 718 |                 var unity = servers.unityMCP ?? servers.UnityMCP;
 719 |                 if (unity == null) return false;
 720 |                 var args = unity.args;
 721 |                 if (args == null) return false;
 722 |                 // Prefer exact extraction of the --directory value and compare normalized paths
 723 |                 string[] strArgs = ((System.Collections.Generic.IEnumerable<object>)args)
 724 |                     .Select(x => x?.ToString() ?? string.Empty)
 725 |                     .ToArray();
 726 |                 string dir = McpConfigFileHelper.ExtractDirectoryArg(strArgs);
 727 |                 if (string.IsNullOrEmpty(dir)) return false;
 728 |                 return McpConfigFileHelper.PathsEqual(dir, pythonDir);
 729 |             }
 730 |             catch { return false; }
 731 |         }
 732 | 
 733 |         private static bool IsClaudeConfigured()
 734 |         {
 735 |             try
 736 |             {
 737 |                 string claudePath = ExecPath.ResolveClaude();
 738 |                 if (string.IsNullOrEmpty(claudePath)) return false;
 739 | 
 740 |                 // Only prepend PATH on Unix
 741 |                 string pathPrepend = null;
 742 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
 743 |                 {
 744 |                     pathPrepend = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
 745 |                         ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
 746 |                         : "/usr/local/bin:/usr/bin:/bin";
 747 |                 }
 748 | 
 749 |                 if (!ExecPath.TryRun(claudePath, "mcp list", workingDir: null, out var stdout, out var stderr, 5000, pathPrepend))
 750 |                 {
 751 |                     return false;
 752 |                 }
 753 |                 return (stdout ?? string.Empty).IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0;
 754 |             }
 755 |             catch { return false; }
 756 |         }
 757 | 
 758 |         private static bool VerifyBridgePing(int port)
 759 |         {
 760 |             // Use strict framed protocol to match bridge (FRAMING=1)
 761 |             const int ConnectTimeoutMs = 1000;
 762 |             const int FrameTimeoutMs = 30000; // match bridge frame I/O timeout
 763 | 
 764 |             try
 765 |             {
 766 |                 using TcpClient client = new TcpClient();
 767 |                 var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
 768 |                 if (!connectTask.Wait(ConnectTimeoutMs)) return false;
 769 | 
 770 |                 using NetworkStream stream = client.GetStream();
 771 |                 try { client.NoDelay = true; } catch { }
 772 | 
 773 |                 // 1) Read handshake line (ASCII, newline-terminated)
 774 |                 string handshake = ReadLineAscii(stream, 2000);
 775 |                 if (string.IsNullOrEmpty(handshake) || handshake.IndexOf("FRAMING=1", StringComparison.OrdinalIgnoreCase) < 0)
 776 |                 {
 777 |                     UnityEngine.Debug.LogWarning("MCP for Unity: Bridge handshake missing FRAMING=1");
 778 |                     return false;
 779 |                 }
 780 | 
 781 |                 // 2) Send framed "ping"
 782 |                 byte[] payload = Encoding.UTF8.GetBytes("ping");
 783 |                 WriteFrame(stream, payload, FrameTimeoutMs);
 784 | 
 785 |                 // 3) Read framed response and check for pong
 786 |                 string response = ReadFrameUtf8(stream, FrameTimeoutMs);
 787 |                 bool ok = !string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0;
 788 |                 if (!ok)
 789 |                 {
 790 |                     UnityEngine.Debug.LogWarning($"MCP for Unity: Framed ping failed; response='{response}'");
 791 |                 }
 792 |                 return ok;
 793 |             }
 794 |             catch (Exception ex)
 795 |             {
 796 |                 UnityEngine.Debug.LogWarning($"MCP for Unity: VerifyBridgePing error: {ex.Message}");
 797 |                 return false;
 798 |             }
 799 |         }
 800 | 
 801 |         // Minimal framing helpers (8-byte big-endian length prefix), blocking with timeouts
 802 |         private static void WriteFrame(NetworkStream stream, byte[] payload, int timeoutMs)
 803 |         {
 804 |             if (payload == null) throw new ArgumentNullException(nameof(payload));
 805 |             if (payload.LongLength < 1) throw new IOException("Zero-length frames are not allowed");
 806 |             byte[] header = new byte[8];
 807 |             ulong len = (ulong)payload.LongLength;
 808 |             header[0] = (byte)(len >> 56);
 809 |             header[1] = (byte)(len >> 48);
 810 |             header[2] = (byte)(len >> 40);
 811 |             header[3] = (byte)(len >> 32);
 812 |             header[4] = (byte)(len >> 24);
 813 |             header[5] = (byte)(len >> 16);
 814 |             header[6] = (byte)(len >> 8);
 815 |             header[7] = (byte)(len);
 816 | 
 817 |             stream.WriteTimeout = timeoutMs;
 818 |             stream.Write(header, 0, header.Length);
 819 |             stream.Write(payload, 0, payload.Length);
 820 |         }
 821 | 
 822 |         private static string ReadFrameUtf8(NetworkStream stream, int timeoutMs)
 823 |         {
 824 |             byte[] header = ReadExact(stream, 8, timeoutMs);
 825 |             ulong len = ((ulong)header[0] << 56)
 826 |                       | ((ulong)header[1] << 48)
 827 |                       | ((ulong)header[2] << 40)
 828 |                       | ((ulong)header[3] << 32)
 829 |                       | ((ulong)header[4] << 24)
 830 |                       | ((ulong)header[5] << 16)
 831 |                       | ((ulong)header[6] << 8)
 832 |                       | header[7];
 833 |             if (len == 0UL) throw new IOException("Zero-length frames are not allowed");
 834 |             if (len > int.MaxValue) throw new IOException("Frame too large");
 835 |             byte[] payload = ReadExact(stream, (int)len, timeoutMs);
 836 |             return Encoding.UTF8.GetString(payload);
 837 |         }
 838 | 
 839 |         private static byte[] ReadExact(NetworkStream stream, int count, int timeoutMs)
 840 |         {
 841 |             byte[] buffer = new byte[count];
 842 |             int offset = 0;
 843 |             stream.ReadTimeout = timeoutMs;
 844 |             while (offset < count)
 845 |             {
 846 |                 int read = stream.Read(buffer, offset, count - offset);
 847 |                 if (read <= 0) throw new IOException("Connection closed before reading expected bytes");
 848 |                 offset += read;
 849 |             }
 850 |             return buffer;
 851 |         }
 852 | 
 853 |         private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int maxLen = 512)
 854 |         {
 855 |             stream.ReadTimeout = timeoutMs;
 856 |             using var ms = new MemoryStream();
 857 |             byte[] one = new byte[1];
 858 |             while (ms.Length < maxLen)
 859 |             {
 860 |                 int n = stream.Read(one, 0, 1);
 861 |                 if (n <= 0) break;
 862 |                 if (one[0] == (byte)'\n') break;
 863 |                 ms.WriteByte(one[0]);
 864 |             }
 865 |             return Encoding.ASCII.GetString(ms.ToArray());
 866 |         }
 867 | 
 868 |         private void DrawClientConfigurationCompact(McpClient mcpClient)
 869 |         {
 870 |             // Special pre-check for Claude Code: if CLI missing, reflect in status UI
 871 |             if (mcpClient.mcpType == McpTypes.ClaudeCode)
 872 |             {
 873 |                 string claudeCheck = ExecPath.ResolveClaude();
 874 |                 if (string.IsNullOrEmpty(claudeCheck))
 875 |                 {
 876 |                     mcpClient.configStatus = "Claude Not Found";
 877 |                     mcpClient.status = McpStatus.NotConfigured;
 878 |                 }
 879 |             }
 880 | 
 881 |             // Pre-check for clients that require uv (all except Claude Code)
 882 |             bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode;
 883 |             bool uvMissingEarly = false;
 884 |             if (uvRequired)
 885 |             {
 886 |                 string uvPathEarly = FindUvPath();
 887 |                 if (string.IsNullOrEmpty(uvPathEarly))
 888 |                 {
 889 |                     uvMissingEarly = true;
 890 |                     mcpClient.configStatus = "uv Not Found";
 891 |                     mcpClient.status = McpStatus.NotConfigured;
 892 |                 }
 893 |             }
 894 | 
 895 |             // Status display
 896 |             EditorGUILayout.BeginHorizontal();
 897 |             Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
 898 |             Color statusColor = GetStatusColor(mcpClient.status);
 899 |             DrawStatusDot(statusRect, statusColor, 16);
 900 | 
 901 |             GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label)
 902 |             {
 903 |                 fontSize = 12,
 904 |                 fontStyle = FontStyle.Bold
 905 |             };
 906 |             EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28));
 907 |             EditorGUILayout.EndHorizontal();
 908 |             // When Claude CLI is missing, show a clear install hint directly below status
 909 |             if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude()))
 910 |             {
 911 |                 GUIStyle installHintStyle = new GUIStyle(clientStatusStyle);
 912 |                 installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange
 913 |                 EditorGUILayout.BeginHorizontal();
 914 |                 GUIContent installText = new GUIContent("Make sure Claude Code is installed!");
 915 |                 Vector2 textSize = installHintStyle.CalcSize(installText);
 916 |                 EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false));
 917 |                 GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold };
 918 |                 GUILayout.Space(6);
 919 |                 if (GUILayout.Button("[HELP]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false)))
 920 |                 {
 921 |                     Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code");
 922 |                 }
 923 |                 EditorGUILayout.EndHorizontal();
 924 |             }
 925 | 
 926 |             EditorGUILayout.Space(10);
 927 | 
 928 |             // If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls
 929 |             if (uvRequired && uvMissingEarly)
 930 |             {
 931 |                 GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label)
 932 |                 {
 933 |                     fontSize = 12,
 934 |                     fontStyle = FontStyle.Bold,
 935 |                     wordWrap = false
 936 |                 };
 937 |                 installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f);
 938 |                 EditorGUILayout.BeginHorizontal();
 939 |                 GUIContent installText2 = new GUIContent("Make sure uv is installed!");
 940 |                 Vector2 sz = installHintStyle2.CalcSize(installText2);
 941 |                 EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false));
 942 |                 GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold };
 943 |                 GUILayout.Space(6);
 944 |                 if (GUILayout.Button("[HELP]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false)))
 945 |                 {
 946 |                     Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf");
 947 |                 }
 948 |                 EditorGUILayout.EndHorizontal();
 949 | 
 950 |                 EditorGUILayout.Space(8);
 951 |                 EditorGUILayout.BeginHorizontal();
 952 |                 if (GUILayout.Button("Choose uv Install Location", GUILayout.Width(260), GUILayout.Height(22)))
 953 |                 {
 954 |                     string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
 955 |                     string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, "");
 956 |                     if (!string.IsNullOrEmpty(picked))
 957 |                     {
 958 |                         EditorPrefs.SetString("MCPForUnity.UvPath", picked);
 959 |                         ConfigureMcpClient(mcpClient);
 960 |                         Repaint();
 961 |                     }
 962 |                 }
 963 |                 EditorGUILayout.EndHorizontal();
 964 |                 return;
 965 |             }
 966 | 
 967 |             // Action buttons in horizontal layout
 968 |             EditorGUILayout.BeginHorizontal();
 969 | 
 970 |             if (mcpClient.mcpType == McpTypes.VSCode)
 971 |             {
 972 |                 if (GUILayout.Button("Auto Configure", GUILayout.Height(32)))
 973 |                 {
 974 |                     ConfigureMcpClient(mcpClient);
 975 |                 }
 976 |             }
 977 |             else if (mcpClient.mcpType == McpTypes.ClaudeCode)
 978 |             {
 979 |                 bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
 980 |                 if (claudeAvailable)
 981 |                 {
 982 |                     bool isConfigured = mcpClient.status == McpStatus.Configured;
 983 |                     string buttonText = isConfigured ? "Unregister MCP for Unity with Claude Code" : "Register with Claude Code";
 984 |                     if (GUILayout.Button(buttonText, GUILayout.Height(32)))
 985 |                     {
 986 |                         if (isConfigured)
 987 |                         {
 988 |                             UnregisterWithClaudeCode();
 989 |                         }
 990 |                         else
 991 |                         {
 992 |                             string pythonDir = FindPackagePythonDirectory();
 993 |                             RegisterWithClaudeCode(pythonDir);
 994 |                         }
 995 |                     }
 996 |                     // Hide the picker once a valid binary is available
 997 |                     EditorGUILayout.EndHorizontal();
 998 |                     EditorGUILayout.BeginHorizontal();
 999 |                     GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true };
1000 |                     string resolvedClaude = ExecPath.ResolveClaude();
1001 |                     EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle);
1002 |                     EditorGUILayout.EndHorizontal();
1003 |                     EditorGUILayout.BeginHorizontal();
1004 |                 }
1005 |                 // CLI picker row (only when not found)
1006 |                 EditorGUILayout.EndHorizontal();
1007 |                 EditorGUILayout.BeginHorizontal();
1008 |                 if (!claudeAvailable)
1009 |                 {
1010 |                     // Only show the picker button in not-found state (no redundant "not found" label)
1011 |                     if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22)))
1012 |                     {
1013 |                         string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
1014 |                         string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, "");
1015 |                         if (!string.IsNullOrEmpty(picked))
1016 |                         {
1017 |                             ExecPath.SetClaudeCliPath(picked);
1018 |                             // Auto-register after setting a valid path
1019 |                             string pythonDir = FindPackagePythonDirectory();
1020 |                             RegisterWithClaudeCode(pythonDir);
1021 |                             Repaint();
1022 |                         }
1023 |                     }
1024 |                 }
1025 |                 EditorGUILayout.EndHorizontal();
1026 |                 EditorGUILayout.BeginHorizontal();
1027 |             }
1028 |             else
1029 |             {
1030 |                 if (GUILayout.Button($"Auto Configure", GUILayout.Height(32)))
1031 |                 {
1032 |                     ConfigureMcpClient(mcpClient);
1033 |                 }
1034 |             }
1035 | 
1036 |             if (mcpClient.mcpType != McpTypes.ClaudeCode)
1037 |             {
1038 |                 if (GUILayout.Button("Manual Setup", GUILayout.Height(32)))
1039 |                 {
1040 |                     string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
1041 |                         ? mcpClient.windowsConfigPath
1042 |                         : mcpClient.linuxConfigPath;
1043 | 
1044 |                     if (mcpClient.mcpType == McpTypes.VSCode)
1045 |                     {
1046 |                         string pythonDir = FindPackagePythonDirectory();
1047 |                         string uvPath = FindUvPath();
1048 |                         if (uvPath == null)
1049 |                         {
1050 |                             UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode.");
1051 |                             return;
1052 |                         }
1053 |                         // VSCode now reads from mcp.json with a top-level "servers" block
1054 |                         var vscodeConfig = new
1055 |                         {
1056 |                             servers = new
1057 |                             {
1058 |                                 unityMCP = new
1059 |                                 {
1060 |                                     command = uvPath,
1061 |                                     args = new[] { "run", "--directory", pythonDir, "server.py" }
1062 |                                 }
1063 |                             }
1064 |                         };
1065 |                         JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
1066 |                         string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
1067 |                         VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson);
1068 |                     }
1069 |                     else
1070 |                     {
1071 |                         ShowManualInstructionsWindow(configPath, mcpClient);
1072 |                     }
1073 |                 }
1074 |             }
1075 | 
1076 |             EditorGUILayout.EndHorizontal();
1077 | 
1078 |             EditorGUILayout.Space(8);
1079 |             // Quick info (hide when Claude is not found to avoid confusion)
1080 |             bool hideConfigInfo =
1081 |                 (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude()))
1082 |                 || ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath()));
1083 |             if (!hideConfigInfo)
1084 |             {
1085 |                 GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel)
1086 |                 {
1087 |                     fontSize = 10
1088 |                 };
1089 |                 EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle);
1090 |             }
1091 |         }
1092 | 
1093 |         private void ToggleUnityBridge()
1094 |         {
1095 |             if (isUnityBridgeRunning)
1096 |             {
1097 |                 MCPForUnityBridge.Stop();
1098 |             }
1099 |             else
1100 |             {
1101 |                 MCPForUnityBridge.Start();
1102 |             }
1103 |             // Reflect the actual state post-operation (avoid optimistic toggle)
1104 |             isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
1105 |             Repaint();
1106 |         }
1107 | 
1108 |         // New method to show manual instructions without changing status
1109 |         private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient)
1110 |         {
1111 |             // Get the Python directory path using Package Manager API
1112 |             string pythonDir = FindPackagePythonDirectory();
1113 |             // Build manual JSON centrally using the shared builder
1114 |             string uvPathForManual = FindUvPath();
1115 |             if (uvPathForManual == null)
1116 |             {
1117 |                 UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration.");
1118 |                 return;
1119 |             }
1120 | 
1121 |             string manualConfig = mcpClient?.mcpType == McpTypes.Codex
1122 |                 ? CodexConfigHelper.BuildCodexServerBlock(uvPathForManual, McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)).TrimEnd() + Environment.NewLine
1123 |                 : ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient);
1124 |             ManualConfigEditorWindow.ShowWindow(configPath, manualConfig, mcpClient);
1125 |         }
1126 | 
1127 |         private string FindPackagePythonDirectory()
1128 |         {
1129 |             // Use shared helper for consistent path resolution across both windows
1130 |             return McpPathResolver.FindPackagePythonDirectory(debugLogsEnabled);
1131 |         }
1132 | 
1133 |         private string ConfigureMcpClient(McpClient mcpClient)
1134 |         {
1135 |             try
1136 |             {
1137 |                 // Use shared helper for consistent config path resolution
1138 |                 string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient);
1139 | 
1140 |                 // Create directory if it doesn't exist
1141 |                 McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
1142 | 
1143 |                 // Find the server.py file location using shared helper
1144 |                 string pythonDir = FindPackagePythonDirectory();
1145 | 
1146 |                 if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))
1147 |                 {
1148 |                     ShowManualInstructionsWindow(configPath, mcpClient);
1149 |                     return "Manual Configuration Required";
1150 |                 }
1151 | 
1152 |                 string result = mcpClient.mcpType == McpTypes.Codex
1153 |                     ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient)
1154 |                     : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient);
1155 | 
1156 |                 // Update the client status after successful configuration
1157 |                 if (result == "Configured successfully")
1158 |                 {
1159 |                     mcpClient.SetStatus(McpStatus.Configured);
1160 |                 }
1161 | 
1162 |                 return result;
1163 |             }
1164 |             catch (Exception e)
1165 |             {
1166 |                 // Determine the config file path based on OS for error message
1167 |                 string configPath = "";
1168 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
1169 |                 {
1170 |                     configPath = mcpClient.windowsConfigPath;
1171 |                 }
1172 |                 else if (
1173 |                     RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
1174 |                 )
1175 |                 {
1176 |                     configPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
1177 |                         ? mcpClient.linuxConfigPath
1178 |                         : mcpClient.macConfigPath;
1179 |                 }
1180 |                 else if (
1181 |                     RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
1182 |                 )
1183 |                 {
1184 |                     configPath = mcpClient.linuxConfigPath;
1185 |                 }
1186 | 
1187 |                 ShowManualInstructionsWindow(configPath, mcpClient);
1188 |                 UnityEngine.Debug.LogError(
1189 |                     $"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}"
1190 |                 );
1191 |                 return $"Failed to configure {mcpClient.name}";
1192 |             }
1193 |         }
1194 | 
1195 |         private void LoadValidationLevelSetting()
1196 |         {
1197 |             string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard");
1198 |             validationLevelIndex = savedLevel.ToLower() switch
1199 |             {
1200 |                 "basic" => 0,
1201 |                 "standard" => 1,
1202 |                 "comprehensive" => 2,
1203 |                 "strict" => 3,
1204 |                 _ => 1 // Default to Standard
1205 |             };
1206 |         }
1207 | 
1208 |         private void SaveValidationLevelSetting()
1209 |         {
1210 |             string levelString = validationLevelIndex switch
1211 |             {
1212 |                 0 => "basic",
1213 |                 1 => "standard",
1214 |                 2 => "comprehensive",
1215 |                 3 => "strict",
1216 |                 _ => "standard"
1217 |             };
1218 |             EditorPrefs.SetString("MCPForUnity_ScriptValidationLevel", levelString);
1219 |         }
1220 | 
1221 |         private string GetValidationLevelDescription(int index)
1222 |         {
1223 |             return index switch
1224 |             {
1225 |                 0 => "Only basic syntax checks (braces, quotes, comments)",
1226 |                 1 => "Syntax checks + Unity best practices and warnings",
1227 |                 2 => "All checks + semantic analysis and performance warnings",
1228 |                 3 => "Full semantic validation with namespace/type resolution (requires Roslyn)",
1229 |                 _ => "Standard validation"
1230 |             };
1231 |         }
1232 | 
1233 |         private void CheckMcpConfiguration(McpClient mcpClient)
1234 |         {
1235 |             try
1236 |             {
1237 |                 // Special handling for Claude Code
1238 |                 if (mcpClient.mcpType == McpTypes.ClaudeCode)
1239 |                 {
1240 |                     CheckClaudeCodeConfiguration(mcpClient);
1241 |                     return;
1242 |                 }
1243 | 
1244 |                 // Use shared helper for consistent config path resolution
1245 |                 string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient);
1246 | 
1247 |                 if (!File.Exists(configPath))
1248 |                 {
1249 |                     mcpClient.SetStatus(McpStatus.NotConfigured);
1250 |                     return;
1251 |                 }
1252 | 
1253 |                 string configJson = File.ReadAllText(configPath);
1254 |                 // Use the same path resolution as configuration to avoid false "Incorrect Path" in dev mode
1255 |                 string pythonDir = FindPackagePythonDirectory();
1256 | 
1257 |                 // Use switch statement to handle different client types, extracting common logic
1258 |                 string[] args = null;
1259 |                 bool configExists = false;
1260 | 
1261 |                 switch (mcpClient.mcpType)
1262 |                 {
1263 |                     case McpTypes.VSCode:
1264 |                         dynamic config = JsonConvert.DeserializeObject(configJson);
1265 | 
1266 |                         // New schema: top-level servers
1267 |                         if (config?.servers?.unityMCP != null)
1268 |                         {
1269 |                             args = config.servers.unityMCP.args.ToObject<string[]>();
1270 |                             configExists = true;
1271 |                         }
1272 |                         // Back-compat: legacy mcp.servers
1273 |                         else if (config?.mcp?.servers?.unityMCP != null)
1274 |                         {
1275 |                             args = config.mcp.servers.unityMCP.args.ToObject<string[]>();
1276 |                             configExists = true;
1277 |                         }
1278 |                         break;
1279 | 
1280 |                     case McpTypes.Codex:
1281 |                         if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs))
1282 |                         {
1283 |                             args = codexArgs;
1284 |                             configExists = true;
1285 |                         }
1286 |                         break;
1287 | 
1288 |                     default:
1289 |                         // Standard MCP configuration check for Claude Desktop, Cursor, etc.
1290 |                         McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
1291 | 
1292 |                         if (standardConfig?.mcpServers?.unityMCP != null)
1293 |                         {
1294 |                             args = standardConfig.mcpServers.unityMCP.args;
1295 |                             configExists = true;
1296 |                         }
1297 |                         break;
1298 |                 }
1299 | 
1300 |                 // Common logic for checking configuration status
1301 |                 if (configExists)
1302 |                 {
1303 |                     string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args);
1304 |                     bool matches = !string.IsNullOrEmpty(configuredDir) && McpConfigFileHelper.PathsEqual(configuredDir, pythonDir);
1305 |                     if (matches)
1306 |                     {
1307 |                         mcpClient.SetStatus(McpStatus.Configured);
1308 |                     }
1309 |                     else
1310 |                     {
1311 |                         // Attempt auto-rewrite once if the package path changed
1312 |                         try
1313 |                         {
1314 |                             string rewriteResult = mcpClient.mcpType == McpTypes.Codex
1315 |                                 ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient)
1316 |                                 : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient);
1317 |                             if (rewriteResult == "Configured successfully")
1318 |                             {
1319 |                                 if (debugLogsEnabled)
1320 |                                 {
1321 |                                     MCPForUnity.Editor.Helpers.McpLog.Info($"Auto-updated MCP config for '{mcpClient.name}' to new path: {pythonDir}", always: false);
1322 |                                 }
1323 |                                 mcpClient.SetStatus(McpStatus.Configured);
1324 |                             }
1325 |                             else
1326 |                             {
1327 |                                 mcpClient.SetStatus(McpStatus.IncorrectPath);
1328 |                             }
1329 |                         }
1330 |                         catch (Exception ex)
1331 |                         {
1332 |                             mcpClient.SetStatus(McpStatus.IncorrectPath);
1333 |                             if (debugLogsEnabled)
1334 |                             {
1335 |                                 UnityEngine.Debug.LogWarning($"MCP for Unity: Auto-config rewrite failed for '{mcpClient.name}': {ex.Message}");
1336 |                             }
1337 |                         }
1338 |                     }
1339 |                 }
1340 |                 else
1341 |                 {
1342 |                     mcpClient.SetStatus(McpStatus.MissingConfig);
1343 |                 }
1344 |             }
1345 |             catch (Exception e)
1346 |             {
1347 |                 mcpClient.SetStatus(McpStatus.Error, e.Message);
1348 |             }
1349 |         }
1350 | 
1351 |         private void RegisterWithClaudeCode(string pythonDir)
1352 |         {
1353 |             // Resolve claude and uv; then run register command
1354 |             string claudePath = ExecPath.ResolveClaude();
1355 |             if (string.IsNullOrEmpty(claudePath))
1356 |             {
1357 |                 UnityEngine.Debug.LogError("MCP for Unity: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
1358 |                 return;
1359 |             }
1360 |             string uvPath = ExecPath.ResolveUv() ?? "uv";
1361 | 
1362 |             // Prefer embedded/dev path when available
1363 |             string srcDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
1364 |             if (string.IsNullOrEmpty(srcDir)) srcDir = pythonDir;
1365 | 
1366 |             string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{srcDir}\" server.py";
1367 | 
1368 |             string projectDir = Path.GetDirectoryName(Application.dataPath);
1369 |             // Ensure PATH includes common locations on Unix; on Windows leave PATH as-is
1370 |             string pathPrepend = null;
1371 |             if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor)
1372 |             {
1373 |                 pathPrepend = Application.platform == RuntimePlatform.OSXEditor
1374 |                     ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
1375 |                     : "/usr/local/bin:/usr/bin:/bin";
1376 |             }
1377 |             if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
1378 |             {
1379 |                 string combined = ($"{stdout}\n{stderr}") ?? string.Empty;
1380 |                 if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0)
1381 |                 {
1382 |                     // Treat as success if Claude reports existing registration
1383 |                     var existingClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
1384 |                     if (existingClient != null) CheckClaudeCodeConfiguration(existingClient);
1385 |                     Repaint();
1386 |                     UnityEngine.Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCP for Unity already registered with Claude Code.");
1387 |                 }
1388 |                 else
1389 |                 {
1390 |                     UnityEngine.Debug.LogError($"MCP for Unity: Failed to start Claude CLI.\n{stderr}\n{stdout}");
1391 |                 }
1392 |                 return;
1393 |             }
1394 | 
1395 |             // Update status
1396 |             var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
1397 |             if (claudeClient != null) CheckClaudeCodeConfiguration(claudeClient);
1398 |             Repaint();
1399 |             UnityEngine.Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Registered with Claude Code.");
1400 |         }
1401 | 
1402 |         private void UnregisterWithClaudeCode()
1403 |         {
1404 |             string claudePath = ExecPath.ResolveClaude();
1405 |             if (string.IsNullOrEmpty(claudePath))
1406 |             {
1407 |                 UnityEngine.Debug.LogError("MCP for Unity: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
1408 |                 return;
1409 |             }
1410 | 
1411 |             string projectDir = Path.GetDirectoryName(Application.dataPath);
1412 |             string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
1413 |                 ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
1414 |                 : null; // On Windows, don't modify PATH - use system PATH as-is
1415 | 
1416 |             // Determine if Claude has a "UnityMCP" server registered by using exit codes from `claude mcp get <name>`
1417 |             string[] candidateNamesForGet = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
1418 |             List<string> existingNames = new List<string>();
1419 |             foreach (var candidate in candidateNamesForGet)
1420 |             {
1421 |                 if (ExecPath.TryRun(claudePath, $"mcp get {candidate}", projectDir, out var getStdout, out var getStderr, 7000, pathPrepend))
1422 |                 {
1423 |                     // Success exit code indicates the server exists
1424 |                     existingNames.Add(candidate);
1425 |                 }
1426 |             }
1427 | 
1428 |             if (existingNames.Count == 0)
1429 |             {
1430 |                 // Nothing to unregister – set status and bail early
1431 |                 var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
1432 |                 if (claudeClient != null)
1433 |                 {
1434 |                     claudeClient.SetStatus(McpStatus.NotConfigured);
1435 |                     UnityEngine.Debug.Log("Claude CLI reports no MCP for Unity server via 'mcp get' - setting status to NotConfigured and aborting unregister.");
1436 |                     Repaint();
1437 |                 }
1438 |                 return;
1439 |             }
1440 | 
1441 |             // Try different possible server names
1442 |             string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
1443 |             bool success = false;
1444 | 
1445 |             foreach (string serverName in possibleNames)
1446 |             {
1447 |                 if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
1448 |                 {
1449 |                     success = true;
1450 |                     UnityEngine.Debug.Log($"MCP for Unity: Successfully removed MCP server: {serverName}");
1451 |                     break;
1452 |                 }
1453 |                 else if (!string.IsNullOrEmpty(stderr) &&
1454 |                          !stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase))
1455 |                 {
1456 |                     // If it's not a "not found" error, log it and stop trying
1457 |                     UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}");
1458 |                     break;
1459 |                 }
1460 |             }
1461 | 
1462 |             if (success)
1463 |             {
1464 |                 var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
1465 |                 if (claudeClient != null)
1466 |                 {
1467 |                     // Optimistically flip to NotConfigured; then verify
1468 |                     claudeClient.SetStatus(McpStatus.NotConfigured);
1469 |                     CheckClaudeCodeConfiguration(claudeClient);
1470 |                 }
1471 |                 Repaint();
1472 |                 UnityEngine.Debug.Log("MCP for Unity: MCP server successfully unregistered from Claude Code.");
1473 |             }
1474 |             else
1475 |             {
1476 |                 // If no servers were found to remove, they're already unregistered
1477 |                 // Force status to NotConfigured and update the UI
1478 |                 UnityEngine.Debug.Log("No MCP servers found to unregister - already unregistered.");
1479 |                 var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
1480 |                 if (claudeClient != null)
1481 |                 {
1482 |                     claudeClient.SetStatus(McpStatus.NotConfigured);
1483 |                     CheckClaudeCodeConfiguration(claudeClient);
1484 |                 }
1485 |                 Repaint();
1486 |             }
1487 |         }
1488 | 
1489 |         // Removed unused ParseTextOutput
1490 | 
1491 |         private string FindUvPath()
1492 |         {
1493 |             try { return MCPForUnity.Editor.Helpers.ServerInstaller.FindUvPath(); } catch { return null; }
1494 |         }
1495 | 
1496 |         // Validation and platform-specific scanning are handled by ServerInstaller.FindUvPath()
1497 | 
1498 |         // Windows-specific discovery removed; use ServerInstaller.FindUvPath() instead
1499 | 
1500 |         // Removed unused FindClaudeCommand
1501 | 
1502 |         private void CheckClaudeCodeConfiguration(McpClient mcpClient)
1503 |         {
1504 |             try
1505 |             {
1506 |                 // Get the Unity project directory to check project-specific config
1507 |                 string unityProjectDir = Application.dataPath;
1508 |                 string projectDir = Path.GetDirectoryName(unityProjectDir);
1509 | 
1510 |                 // Read the global Claude config file (honor macConfigPath on macOS)
1511 |                 string configPath;
1512 |                 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
1513 |                     configPath = mcpClient.windowsConfigPath;
1514 |                 else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
1515 |                     configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) ? mcpClient.linuxConfigPath : mcpClient.macConfigPath;
1516 |                 else
1517 |                     configPath = mcpClient.linuxConfigPath;
1518 | 
1519 |                 if (debugLogsEnabled)
1520 |                 {
1521 |                     MCPForUnity.Editor.Helpers.McpLog.Info($"Checking Claude config at: {configPath}", always: false);
1522 |                 }
1523 | 
1524 |                 if (!File.Exists(configPath))
1525 |                 {
1526 |                     UnityEngine.Debug.LogWarning($"Claude config file not found at: {configPath}");
1527 |                     mcpClient.SetStatus(McpStatus.NotConfigured);
1528 |                     return;
1529 |                 }
1530 | 
1531 |                 string configJson = File.ReadAllText(configPath);
1532 |                 dynamic claudeConfig = JsonConvert.DeserializeObject(configJson);
1533 | 
1534 |                 // Check for "UnityMCP" server in the mcpServers section (current format)
1535 |                 if (claudeConfig?.mcpServers != null)
1536 |                 {
1537 |                     var servers = claudeConfig.mcpServers;
1538 |                     if (servers.UnityMCP != null || servers.unityMCP != null)
1539 |                     {
1540 |                         // Found MCP for Unity configured
1541 |                         mcpClient.SetStatus(McpStatus.Configured);
1542 |                         return;
1543 |                     }
1544 |                 }
1545 | 
1546 |                 // Also check if there's a project-specific configuration for this Unity project (legacy format)
1547 |                 if (claudeConfig?.projects != null)
1548 |                 {
1549 |                     // Look for the project path in the config
1550 |                     foreach (var project in claudeConfig.projects)
1551 |                     {
1552 |                         string projectPath = project.Name;
1553 | 
1554 |                         // Normalize paths for comparison (handle forward/back slash differences)
1555 |                         string normalizedProjectPath = Path.GetFullPath(projectPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
1556 |                         string normalizedProjectDir = Path.GetFullPath(projectDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
1557 | 
1558 |                         if (string.Equals(normalizedProjectPath, normalizedProjectDir, StringComparison.OrdinalIgnoreCase) && project.Value?.mcpServers != null)
1559 |                         {
1560 |                             // Check for "UnityMCP" (case variations)
1561 |                             var servers = project.Value.mcpServers;
1562 |                             if (servers.UnityMCP != null || servers.unityMCP != null)
1563 |                             {
1564 |                                 // Found MCP for Unity configured for this project
1565 |                                 mcpClient.SetStatus(McpStatus.Configured);
1566 |                                 return;
1567 |                             }
1568 |                         }
1569 |                     }
1570 |                 }
1571 | 
1572 |                 // No configuration found for this project
1573 |                 mcpClient.SetStatus(McpStatus.NotConfigured);
1574 |             }
1575 |             catch (Exception e)
1576 |             {
1577 |                 UnityEngine.Debug.LogWarning($"Error checking Claude Code config: {e.Message}");
1578 |                 mcpClient.SetStatus(McpStatus.Error, e.Message);
1579 |             }
1580 |         }
1581 | 
1582 |         private void ShowMigrationDialogIfNeeded()
1583 |         {
1584 |             const string dialogShownKey = "MCPForUnity.LegacyMigrationDialogShown";
1585 |             if (EditorPrefs.GetBool(dialogShownKey, false))
1586 |             {
1587 |                 return; // Already shown
1588 |             }
1589 | 
1590 |             int result = EditorUtility.DisplayDialogComplex(
1591 |                 "Migration Required",
1592 |                 "This is the legacy UnityMcpBridge package.\n\n" +
1593 |                 "Please migrate to the new MCPForUnity package to receive updates and support.\n\n" +
1594 |                 "Migration takes just a few minutes.",
1595 |                 "View Migration Guide",
1596 |                 "Remind Me Later",
1597 |                 "I'll Migrate Later"
1598 |             );
1599 | 
1600 |             if (result == 0) // View Migration Guide
1601 |             {
1602 |                 Application.OpenURL("https://github.com/CoplayDev/unity-mcp/blob/main/docs/v5_MIGRATION.md");
1603 |                 EditorPrefs.SetBool(dialogShownKey, true);
1604 |             }
1605 |             else if (result == 2) // I'll Migrate Later
1606 |             {
1607 |                 EditorPrefs.SetBool(dialogShownKey, true);
1608 |             }
1609 |             // result == 1 (Remind Me Later) - don't set the flag, show again next time
1610 |         }
1611 | 
1612 |         private void DrawMigrationWarningBanner()
1613 |         {
1614 |             // Warning banner - not dismissible, always visible
1615 |             EditorGUILayout.Space(5);
1616 |             Rect bannerRect = EditorGUILayout.GetControlRect(false, 50);
1617 |             EditorGUI.DrawRect(bannerRect, new Color(1f, 0.6f, 0f, 0.3f)); // Orange background
1618 | 
1619 |             GUIStyle warningStyle = new GUIStyle(EditorStyles.boldLabel)
1620 |             {
1621 |                 fontSize = 13,
1622 |                 alignment = TextAnchor.MiddleLeft,
1623 |                 richText = true
1624 |             };
1625 | 
1626 |             // Use Unicode warning triangle (same as used elsewhere in codebase at line 647, 652)
1627 |             string warningText = "\u26A0 <color=#FF8C00>LEGACY PACKAGE:</color> Please migrate to MCPForUnity for updates and support.";
1628 | 
1629 |             Rect textRect = new Rect(bannerRect.x + 15, bannerRect.y + 8, bannerRect.width - 180, bannerRect.height - 16);
1630 |             GUI.Label(textRect, warningText, warningStyle);
1631 | 
1632 |             // Button on the right
1633 |             Rect buttonRect = new Rect(bannerRect.xMax - 160, bannerRect.y + 10, 145, 30);
1634 |             if (GUI.Button(buttonRect, "View Migration Guide"))
1635 |             {
1636 |                 Application.OpenURL("https://github.com/CoplayDev/unity-mcp/blob/main/docs/v5_MIGRATION.md");
1637 |             }
1638 |             EditorGUILayout.Space(5);
1639 |         }
1640 | 
1641 |         private bool IsPythonDetected()
1642 |         {
1643 |             try
1644 |             {
1645 |                 // Windows-specific Python detection
1646 |                 if (Application.platform == RuntimePlatform.WindowsEditor)
1647 |                 {
1648 |                     // Common Windows Python installation paths
1649 |                     string[] windowsCandidates =
1650 |                     {
1651 |                         @"C:\Python313\python.exe",
1652 |                         @"C:\Python312\python.exe",
1653 |                         @"C:\Python311\python.exe",
1654 |                         @"C:\Python310\python.exe",
1655 |                         @"C:\Python39\python.exe",
1656 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python313\python.exe"),
1657 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python312\python.exe"),
1658 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python311\python.exe"),
1659 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python310\python.exe"),
1660 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39\python.exe"),
1661 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python313\python.exe"),
1662 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python312\python.exe"),
1663 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python311\python.exe"),
1664 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"),
1665 |                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\python.exe"),
1666 |                     };
1667 | 
1668 |                     foreach (string c in windowsCandidates)
1669 |                     {
1670 |                         if (File.Exists(c)) return true;
1671 |                     }
1672 | 
1673 |                     // Try 'where python' command (Windows equivalent of 'which')
1674 |                     var psi = new ProcessStartInfo
1675 |                     {
1676 |                         FileName = "where",
1677 |                         Arguments = "python",
1678 |                         UseShellExecute = false,
1679 |                         RedirectStandardOutput = true,
1680 |                         RedirectStandardError = true,
1681 |                         CreateNoWindow = true
1682 |                     };
1683 |                     using var p = Process.Start(psi);
1684 |                     string outp = p.StandardOutput.ReadToEnd().Trim();
1685 |                     p.WaitForExit(2000);
1686 |                     if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
1687 |                     {
1688 |                         string[] lines = outp.Split('\n');
1689 |                         foreach (string line in lines)
1690 |                         {
1691 |                             string trimmed = line.Trim();
1692 |                             if (File.Exists(trimmed)) return true;
1693 |                         }
1694 |                     }
1695 |                 }
1696 |                 else
1697 |                 {
1698 |                     // macOS/Linux detection (existing code)
1699 |                     string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
1700 |                     string[] candidates =
1701 |                     {
1702 |                         "/opt/homebrew/bin/python3",
1703 |                         "/usr/local/bin/python3",
1704 |                         "/usr/bin/python3",
1705 |                         "/opt/local/bin/python3",
1706 |                         Path.Combine(home, ".local", "bin", "python3"),
1707 |                         "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
1708 |                         "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
1709 |                     };
1710 |                     foreach (string c in candidates)
1711 |                     {
1712 |                         if (File.Exists(c)) return true;
1713 |                     }
1714 | 
1715 |                     // Try 'which python3'
1716 |                     var psi = new ProcessStartInfo
1717 |                     {
1718 |                         FileName = "/usr/bin/which",
1719 |                         Arguments = "python3",
1720 |                         UseShellExecute = false,
1721 |                         RedirectStandardOutput = true,
1722 |                         RedirectStandardError = true,
1723 |                         CreateNoWindow = true
1724 |                     };
1725 |                     using var p = Process.Start(psi);
1726 |                     string outp = p.StandardOutput.ReadToEnd().Trim();
1727 |                     p.WaitForExit(2000);
1728 |                     if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true;
1729 |                 }
1730 |             }
1731 |             catch { }
1732 |             return false;
1733 |         }
1734 |     }
1735 | }
1736 | 
```
Page 14/18FirstPrevNextLast