Skip to main content
Glama
UnityMcpPlugin.Static.cs11.7 kB
/* ┌──────────────────────────────────────────────────────────────────┐ │ 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 System; using System.Threading; using System.Threading.Tasks; using com.IvanMurzak.McpPlugin.Common; using com.IvanMurzak.McpPlugin.Common.Model; using com.IvanMurzak.Unity.MCP.Runtime.Utils; using com.IvanMurzak.Unity.MCP.Utils; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using R3; namespace com.IvanMurzak.Unity.MCP { using ILogger = Microsoft.Extensions.Logging.ILogger; using LogLevel = com.IvanMurzak.Unity.MCP.Runtime.Utils.LogLevel; using MicrosoftLogLevel = Microsoft.Extensions.Logging.LogLevel; public partial class UnityMcpPlugin { static readonly Subject<UnityConnectionConfig> _onConfigChanged = new Subject<UnityConnectionConfig>(); static readonly ILogger _logger = UnityLoggerFactory.LoggerFactory.CreateLogger<UnityMcpPlugin>(); static readonly object _instanceMutex = new(); static UnityMcpPlugin instance = null!; public static bool HasInstance { get { lock (_instanceMutex) { return instance != null; } } } public static UnityMcpPlugin Instance { get { InitSingletonIfNeeded(); lock (_instanceMutex) { return instance; } } } public static void InitSingletonIfNeeded() { lock (_instanceMutex) { if (instance == null) { instance = new UnityMcpPlugin(); if (instance == null) { _logger.LogWarning("{method}: ConnectionConfig instance is null", nameof(InitSingletonIfNeeded)); return; } } } } public static bool IsLogEnabled(LogLevel level) => LogLevel.IsEnabled(level); public static LogLevel LogLevel { get => Instance.unityConnectionConfig.LogLevel; set { Instance.unityConnectionConfig.LogLevel = value; NotifyChanged(Instance.unityConnectionConfig); } } public static string Host { get => Instance.unityConnectionConfig.Host; set { Instance.unityConnectionConfig.Host = value; NotifyChanged(Instance.unityConnectionConfig); } } public static bool KeepConnected { get => Instance.unityConnectionConfig.KeepConnected; set { Instance.unityConnectionConfig.KeepConnected = value; NotifyChanged(Instance.unityConnectionConfig); } } public static int TimeoutMs { get => Instance.unityConnectionConfig.TimeoutMs; set { Instance.unityConnectionConfig.TimeoutMs = value; NotifyChanged(Instance.unityConnectionConfig); } } public static int Port { get { if (Uri.TryCreate(Host, UriKind.Absolute, out var uri) && uri.Port > 0 && uri.Port <= Consts.Hub.MaxPort) return uri.Port; return GeneratePortFromDirectory(); } } static ReactiveProperty<HubConnectionState> _connectionState = new(HubConnectionState.Disconnected); public static ReadOnlyReactiveProperty<HubConnectionState> ConnectionState => _connectionState; public static ReadOnlyReactiveProperty<bool> IsConnected => _connectionState .Select(x => x == HubConnectionState.Connected) .ToReadOnlyReactiveProperty(false); public static async Task NotifyToolRequestCompleted(RequestToolCompletedData request, CancellationToken cancellationToken = default) { var mcpPlugin = Instance.McpPluginInstance ?? throw new InvalidOperationException($"{nameof(Instance.McpPluginInstance)} is null"); // wait when connection will be established while (mcpPlugin.ConnectionState.CurrentValue != HubConnectionState.Connected) { await Task.Delay(100, cancellationToken); if (cancellationToken.IsCancellationRequested) { _logger.LogWarning("{method}: operation cancelled while waiting for connection.", nameof(NotifyToolRequestCompleted)); return; } } if (mcpPlugin.McpManager == null) { _logger.LogCritical("{method}: {instance} is null", nameof(NotifyToolRequestCompleted), nameof(mcpPlugin.McpManager)); return; } if (mcpPlugin.RemoteMcpManagerHub == null) { _logger.LogCritical("{method}: {instance} is null", nameof(NotifyToolRequestCompleted), nameof(mcpPlugin.RemoteMcpManagerHub)); return; } await mcpPlugin.RemoteMcpManagerHub.NotifyToolRequestCompleted(request); } public static IDisposable SubscribeOnChanged(Action<UnityConnectionConfig> action, bool invokeImmediately = true) { if (action == null) throw new ArgumentNullException(nameof(action)); var subscription = _onConfigChanged.Subscribe(action); if (invokeImmediately) Safe.Run(action, Instance.unityConnectionConfig, logLevel: Instance.unityConnectionConfig?.LogLevel ?? LogLevel.Trace); return subscription; } public static Task<bool> ConnectIfNeeded() { if (KeepConnected == false) return Task.FromResult(false); return Connect(); } public static async Task<bool> Connect() { _logger.LogTrace("{method} called.", nameof(Connect)); try { var mcpPlugin = Instance.McpPluginInstance; if (mcpPlugin == null) { _logger.LogError("{method} isInitialized set <false>.", nameof(Connect)); return false; // ignore } return await mcpPlugin.Connect(); } finally { _logger.LogTrace("{method} completed.", nameof(Connect)); } } public async Task Disconnect() { _logger.LogTrace("{method} called.", nameof(Disconnect)); try { var mcpPlugin = McpPluginInstance; if (mcpPlugin == null) { _logger.LogWarning("{method}: McpPlugin instance is null, nothing to disconnect, ignoring.", nameof(Disconnect)); return; } else { try { _logger.LogDebug("{method}: Disconnecting McpPlugin instance.", nameof(Disconnect)); await mcpPlugin.Disconnect(); } catch (Exception e) { _logger.LogError("{method}: Exception during disconnecting: {exception}", nameof(Disconnect), e); return; } } } finally { _logger.LogTrace("{method} completed.", nameof(Disconnect)); } } public void DisconnectImmediate() { _logger.LogTrace("{method} called.", nameof(DisconnectImmediate)); try { var mcpPlugin = McpPluginInstance; if (mcpPlugin == null) { _logger.LogWarning("{method}: McpPlugin instance is null, nothing to disconnect, ignoring.", nameof(DisconnectImmediate)); return; } else { try { _logger.LogDebug("{method}: Disconnecting McpPlugin instance.", nameof(DisconnectImmediate)); mcpPlugin.DisconnectImmediate(); } catch (Exception e) { _logger.LogError("{method}: Exception during disconnecting: {exception}", nameof(DisconnectImmediate), e); } } } finally { _logger.LogTrace("{method} completed.", nameof(DisconnectImmediate)); } } public static void StaticDispose() { _logger.LogTrace("{method} called.", nameof(StaticDispose)); _connectionState.Dispose(); lock (_instanceMutex) { instance?.Dispose(); instance = null!; } } /// <summary> /// Generate a deterministic TCP port based on current directory. /// Uses SHA256 hash for better distribution and less collisions. /// Port range: 50000-59999 (less commonly used dynamic ports). /// </summary> public static int GeneratePortFromDirectory() { const int MinPort = 50000; // Higher range to avoid common dynamic ports const int MaxPort = 59999; const int PortRange = MaxPort - MinPort + 1; var currentDir = System.Environment.CurrentDirectory.ToLowerInvariant(); using (var sha256 = System.Security.Cryptography.SHA256.Create()) { var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(currentDir)); // Use first 4 bytes to create an integer var hash = System.BitConverter.ToInt32(hashBytes, 0); // Map to port range var port = MinPort + (System.Math.Abs(hash) % PortRange); return port; } } static void NotifyChanged(UnityConnectionConfig data) => Safe.Run( action: (x) => _onConfigChanged.OnNext(x), value: data, logLevel: data?.LogLevel ?? LogLevel.Trace); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/IvanMurzak/Unity-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server