/* ┌──────────────────────────────────────────────────────────────────┐ │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ │ Copyright (c) 2025 Ivan Murzak │ │ Licensed under the Apache License, Version 2.0. │ │ See the LICENSE file in the project root for more information. │ └──────────────────────────────────────────────────────────────────┘ */ #nullable enable using com.IvanMurzak.Unity.MCP.Editor.Utils; using Microsoft.Extensions.Logging; namespace com.IvanMurzak.Unity.MCP { public partial class UnityMcpPluginEditor { /// /// Editor-only test accessor for the underlying . /// The field is protected on ; this accessor exposes /// it to the Editor test assembly (via InternalsVisibleTo) so tests can verify /// state that is written through the plugin's build path. /// internal UnityConnectionConfig ConnectionConfigForTests => unityConnectionConfig; public UnityMcpPluginEditor BuildMcpPluginIfNeeded() { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var loggerProvider = BuildLoggerProvider(); // Feed the host project root into the connection config BEFORE the plugin is built. // McpPlugin's ctor calls GenerateSkillFilesIfNeeded() internally; it needs an anchor // for the relative SkillsPath. Without this, the McpPlugin ctor logs an // InvalidOperationException every Editor open / domain reload — see // https://github.com/IvanMurzak/Unity-MCP/issues/766 (host-side cascade of upstream // https://github.com/IvanMurzak/MCP-Plugin-dotnet/issues/107 / // https://github.com/IvanMurzak/MCP-Plugin-dotnet/pull/108). unityConnectionConfig.ProjectRootPath = ProjectRootPath; _logger.LogTrace("Seeded ConnectionConfig.ProjectRootPath={path}", ProjectRootPath); var built = _plugin.BuildOnce(() => BuildMcpPlugin( version: BuildVersion(), reflector: CreateDefaultReflector(), loggerProvider: loggerProvider, configure: builder => { builder.WithSkillFileGenerator(new UnitySkillFileGenerator(loggerProvider?.CreateLogger(nameof(UnitySkillFileGenerator)))); } )); stopwatch.Stop(); if (built == null) return this; // already built, nothing to wire up _logger.LogDebug("MCP Plugin built in {elapsedMilliseconds} ms.", stopwatch.ElapsedMilliseconds); SetCurrentPlugin(built); ApplyConfigToMcpPlugin(built); return this; } public void DisposeMcpPluginInstance() { var oldInstance = _plugin.TakeInstance(); if (oldInstance == null) return; SetCurrentPlugin(null); // Dispose on a background thread to avoid blocking Unity's main thread. // The dispose path calls ConnectionManager.DisconnectImmediate() which // internally blocks on a semaphore (_gate) held by a pending Connect() // task whose continuation is queued on the main thread SynchronizationContext — // a deadlock if we block the main thread here. _ = System.Threading.Tasks.Task.Run(() => oldInstance.Dispose()); } } }