unity-mcp/MCPForUnity/Editor/Services/BridgeControlService.cs

175 lines
6.5 KiB
C#
Raw Normal View History

New UI and work without MCP server embedded (#313) * First pass at new UI * Point to new UI * Refactor: New Service-Based MCP Editor Window Architecture We separate the business logic from the UI rendering of the new editor window with new services. I didn't go full Dependency Injection, not sure if I want to add those deps to the install as yet, but service location is fairly straightforward. Some differences with the old window: - No more Auto-Setup, users will manually decide what they want to do - Removed Python detection warning, we have a setup wizard now - Added explicit path overrides for `uv` and the MCP server itself * style: add flex-shrink and overflow handling to improve UI element scaling * fix: update UI configuration and visibility when client status changes * feat: add menu item to open legacy MCP configuration window * refactor: improve editor window lifecycle handling with proper update subscription * feat: add auto-verification of bridge health when connected * fix: update Claude Code MCP server registration to use lowercase unityMCP name and correct the manual installation instructions * fix: add Claude CLI directory to PATH for node/nvm environments * Clarify how users will see MCP tools * Add a keyboard shortcut to open the window * feat: add server download UI and improve installation status messaging This is needed for the Unity Asset Store, which doesn't have the Python server embedded. * feat: add dynamic asset path detection to support both Package Manager and Asset Store installations * fix: replace unicode emojis with escaped characters in status messages * feat: add server package creation and GitHub release publishing to version bump workflow * fix: add v prefix to server package filename in release workflow * Fix download location * style: improve dropdown and settings layout responsiveness with flex-shrink and max-width * feat: add package.json version detection and refactor path utilities * refactor: simplify imports and use fully qualified names in ServerInstaller.cs * refactor: replace Unity Debug.Log calls with custom McpLog class * fix: extract server files to temp directory before moving to final location * docs: add v6 UI documentation and screenshots with service architecture overview * docs: add new UI Toolkit-based editor window with service architecture and path overrides * feat: improve package path resolution to support Package Manager and Asset Store installations * Change Claude Code's casing back to "UnityMCP" There's no need to break anything as yet * fix: update success dialog text to clarify manual bridge start requirement * refactor: move RefreshDebounce and ManageScriptRefreshHelpers classes inside namespace * feat: add Asset Store fallback path detection for package root lookup * fix: update server installation success message to be more descriptive * refactor: replace Unity Debug.Log calls with custom McpLog utility * fix: add file existence check before opening configuration file * refactor: simplify asset path handling and remove redundant helper namespace references * docs: update code block syntax highlighting in UI changes doc * docs: add code block syntax highlighting for file structure example * feat: import UnityEditor.UIElements namespace for UI components for Unity 2021 compatibility * refactor: rename Python server references to MCP server for consistency * fix: reset client status label color after error state is cleared * Replace the phrase "Python server" with "MCP server" * MInor doc clarification * docs: add path override methods for UV and Claude CLI executables * docs: update service locator registration method name from SetCustomImplementation to Register
2025-10-11 15:08:16 +08:00
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace MCPForUnity.Editor.Services
{
/// <summary>
/// Implementation of bridge control service
/// </summary>
public class BridgeControlService : IBridgeControlService
{
public bool IsRunning => MCPForUnityBridge.IsRunning;
public int CurrentPort => MCPForUnityBridge.GetCurrentPort();
public bool IsAutoConnectMode => MCPForUnityBridge.IsAutoConnectMode();
public void Start()
{
// If server is installed, use auto-connect mode
// Otherwise use standard mode
string serverPath = MCPServiceLocator.Paths.GetMcpServerPath();
if (!string.IsNullOrEmpty(serverPath) && File.Exists(Path.Combine(serverPath, "server.py")))
{
MCPForUnityBridge.StartAutoConnect();
}
else
{
MCPForUnityBridge.Start();
}
}
public void Stop()
{
MCPForUnityBridge.Stop();
}
public BridgeVerificationResult Verify(int port)
{
var result = new BridgeVerificationResult
{
Success = false,
HandshakeValid = false,
PingSucceeded = false,
Message = "Verification not started"
};
const int ConnectTimeoutMs = 1000;
const int FrameTimeoutMs = 30000; // Match bridge frame I/O timeout
try
{
using (var client = new TcpClient())
{
// Attempt connection
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
if (!connectTask.Wait(ConnectTimeoutMs))
{
result.Message = "Connection timeout";
return result;
}
using (var stream = client.GetStream())
{
try { client.NoDelay = true; } catch { }
// 1) Read handshake line (ASCII, newline-terminated)
string handshake = ReadLineAscii(stream, 2000);
if (string.IsNullOrEmpty(handshake) || handshake.IndexOf("FRAMING=1", StringComparison.OrdinalIgnoreCase) < 0)
{
result.Message = "Bridge handshake missing FRAMING=1";
return result;
}
result.HandshakeValid = true;
// 2) Send framed "ping"
byte[] payload = Encoding.UTF8.GetBytes("ping");
WriteFrame(stream, payload, FrameTimeoutMs);
// 3) Read framed response and check for pong
string response = ReadFrameUtf8(stream, FrameTimeoutMs);
if (!string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0)
{
result.PingSucceeded = true;
result.Success = true;
result.Message = "Bridge verified successfully";
}
else
{
result.Message = $"Ping failed; response='{response}'";
}
}
}
}
catch (Exception ex)
{
result.Message = $"Verification error: {ex.Message}";
}
return result;
}
// Minimal framing helpers (8-byte big-endian length prefix), blocking with timeouts
private static void WriteFrame(NetworkStream stream, byte[] payload, int timeoutMs)
{
if (payload == null) throw new ArgumentNullException(nameof(payload));
if (payload.LongLength < 1) throw new IOException("Zero-length frames are not allowed");
byte[] header = new byte[8];
ulong len = (ulong)payload.LongLength;
header[0] = (byte)(len >> 56);
header[1] = (byte)(len >> 48);
header[2] = (byte)(len >> 40);
header[3] = (byte)(len >> 32);
header[4] = (byte)(len >> 24);
header[5] = (byte)(len >> 16);
header[6] = (byte)(len >> 8);
header[7] = (byte)(len);
stream.WriteTimeout = timeoutMs;
stream.Write(header, 0, header.Length);
stream.Write(payload, 0, payload.Length);
}
private static string ReadFrameUtf8(NetworkStream stream, int timeoutMs)
{
byte[] header = ReadExact(stream, 8, timeoutMs);
ulong len = ((ulong)header[0] << 56)
| ((ulong)header[1] << 48)
| ((ulong)header[2] << 40)
| ((ulong)header[3] << 32)
| ((ulong)header[4] << 24)
| ((ulong)header[5] << 16)
| ((ulong)header[6] << 8)
| header[7];
if (len == 0UL) throw new IOException("Zero-length frames are not allowed");
if (len > int.MaxValue) throw new IOException("Frame too large");
byte[] payload = ReadExact(stream, (int)len, timeoutMs);
return Encoding.UTF8.GetString(payload);
}
private static byte[] ReadExact(NetworkStream stream, int count, int timeoutMs)
{
byte[] buffer = new byte[count];
int offset = 0;
stream.ReadTimeout = timeoutMs;
while (offset < count)
{
int read = stream.Read(buffer, offset, count - offset);
if (read <= 0) throw new IOException("Connection closed before reading expected bytes");
offset += read;
}
return buffer;
}
private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int maxLen = 512)
{
stream.ReadTimeout = timeoutMs;
using (var ms = new MemoryStream())
{
byte[] one = new byte[1];
while (ms.Length < maxLen)
{
int n = stream.Read(one, 0, 1);
if (n <= 0) break;
if (one[0] == (byte)'\n') break;
ms.WriteByte(one[0]);
}
return Encoding.ASCII.GetString(ms.ToArray());
}
}
}
}