fix: linux XDG config paths; prefer installed server; robust cursor detection; atomic writes; uv validation; WinGet Links ordering
parent
6e22721d3a
commit
6e59b8fe8d
|
|
@ -0,0 +1,85 @@
|
||||||
|
### Cursor/VSCode/Windsurf: UV path issue on Windows (diagnosis and fix)
|
||||||
|
|
||||||
|
#### The issue
|
||||||
|
- Some Windows machines have multiple `uv.exe` locations. Our auto-config sometimes picked a less stable path, causing the MCP client to fail to launch the Unity MCP Server or for the path to be auto-rewritten on repaint/restart.
|
||||||
|
|
||||||
|
#### Typical symptoms
|
||||||
|
- Cursor shows the UnityMCP server but never connects or reports it “can’t start.”
|
||||||
|
- Your `%USERPROFILE%\\.cursor\\mcp.json` flips back to a different `command` path when Unity or the Unity MCP window refreshes.
|
||||||
|
|
||||||
|
#### Real-world example
|
||||||
|
- Wrong/fragile path (auto-picked):
|
||||||
|
- `C:\Users\mrken.local\bin\uv.exe` (malformed, not standard)
|
||||||
|
- `C:\Users\mrken\AppData\Local\Microsoft\WinGet\Packages\astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe\uv.exe`
|
||||||
|
- Correct/stable path (works with Cursor):
|
||||||
|
- `C:\Users\mrken\AppData\Local\Microsoft\WinGet\Links\uv.exe`
|
||||||
|
|
||||||
|
#### Quick fix (recommended)
|
||||||
|
1) In Unity: `Window > Unity MCP` → select your MCP client (Cursor or Windsurf)
|
||||||
|
2) If you see “uv Not Found,” click “Choose UV Install Location” and browse to:
|
||||||
|
- `C:\Users\<YOU>\AppData\Local\Microsoft\WinGet\Links\uv.exe`
|
||||||
|
3) If uv is already found but wrong, still click “Choose UV Install Location” and select the `Links\uv.exe` path above. This saves a persistent override.
|
||||||
|
4) Click “Auto Configure” (or re-open the client) and restart Cursor.
|
||||||
|
|
||||||
|
This sets an override stored in the Editor (key: `UnityMCP.UvPath`) so UnityMCP won’t auto-rewrite the config back to a different `uv.exe` later.
|
||||||
|
|
||||||
|
#### Verify the fix
|
||||||
|
- Confirm global Cursor config is at: `%USERPROFILE%\\.cursor\\mcp.json`
|
||||||
|
- You should see something like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"unityMCP": {
|
||||||
|
"command": "C:\\Users\\YOU\\AppData\\Local\\Microsoft\\WinGet\\Links\\uv.exe",
|
||||||
|
"args": [
|
||||||
|
"--directory",
|
||||||
|
"C:\\Users\\YOU\\AppData\\Local\\Programs\\UnityMCP\\UnityMcpServer\\src",
|
||||||
|
"run",
|
||||||
|
"server.py"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Manually run the same command in PowerShell to confirm it launches:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
"C:\Users\YOU\AppData\Local\Microsoft\WinGet\Links\uv.exe" --directory "C:\Users\YOU\AppData\Local\Programs\UnityMCP\UnityMcpServer\src" run server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
If that runs without error, restart Cursor and it should connect.
|
||||||
|
|
||||||
|
#### Why this happens
|
||||||
|
- On Windows, multiple `uv.exe` can exist (WinGet Packages path, a WinGet Links shim, Python Scripts, etc.). The Links shim is the most stable target for GUI apps to launch.
|
||||||
|
- Prior versions of the auto-config could pick the first found path and re-write config on refresh. Choosing a path via the MCP window pins a known‑good absolute path and prevents auto-rewrites.
|
||||||
|
|
||||||
|
#### Extra notes
|
||||||
|
- Restart Cursor after changing `mcp.json`; it doesn’t always hot-reload that file.
|
||||||
|
- If you also have a project-scoped `.cursor\\mcp.json` in your Unity project folder, that file overrides the global one.
|
||||||
|
|
||||||
|
|
||||||
|
### Why pin the WinGet Links shim (and not the Packages path)
|
||||||
|
|
||||||
|
- Windows often has multiple `uv.exe` installs and GUI clients (Cursor/Windsurf/VSCode) may launch with a reduced `PATH`. Using an absolute path is safer than `"command": "uv"`.
|
||||||
|
- WinGet publishes stable launch shims in these locations:
|
||||||
|
- User scope: `%LOCALAPPDATA%\Microsoft\WinGet\Links\uv.exe`
|
||||||
|
- Machine scope: `C:\Program Files\WinGet\Links\uv.exe`
|
||||||
|
These shims survive upgrades and are intended as the portable entrypoints. See the WinGet notes: [discussion](https://github.com/microsoft/winget-pkgs/discussions/184459) • [how to find installs](https://superuser.com/questions/1739292/how-to-know-where-winget-installed-a-program)
|
||||||
|
- The `Packages` root is where payloads live and can change across updates, so avoid pointing your config at it.
|
||||||
|
|
||||||
|
Recommended practice
|
||||||
|
|
||||||
|
- Prefer the WinGet Links shim paths above. If present, select one via “Choose UV Install Location”.
|
||||||
|
- If the unity window keeps rewriting to a different `uv.exe`, pick the Links shim again; Unity MCP saves a pinned override and will stop auto-rewrites.
|
||||||
|
- If neither Links path exists, a reasonable fallback is `~/.local/bin/uv.exe` (uv tools bin) or a Scoop shim, but Links is preferred for stability.
|
||||||
|
|
||||||
|
References
|
||||||
|
|
||||||
|
- WinGet portable Links: [GitHub discussion](https://github.com/microsoft/winget-pkgs/discussions/184459)
|
||||||
|
- WinGet install locations: [Super User](https://superuser.com/questions/1739292/how-to-know-where-winget-installed-a-program)
|
||||||
|
- GUI client PATH caveats (Cursor): [Cursor community thread](https://forum.cursor.com/t/mcp-feature-client-closed-fix/54651?page=4)
|
||||||
|
- uv tools install location (`~/.local/bin`): [Astral docs](https://docs.astral.sh/uv/concepts/tools/)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,8 +71,7 @@ namespace UnityMcpBridge.Editor.Data
|
||||||
),
|
),
|
||||||
linuxConfigPath = Path.Combine(
|
linuxConfigPath = Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
"Library",
|
".config",
|
||||||
"Application Support",
|
|
||||||
"Claude",
|
"Claude",
|
||||||
"claude_desktop_config.json"
|
"claude_desktop_config.json"
|
||||||
),
|
),
|
||||||
|
|
@ -91,8 +90,7 @@ namespace UnityMcpBridge.Editor.Data
|
||||||
),
|
),
|
||||||
linuxConfigPath = Path.Combine(
|
linuxConfigPath = Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
"Library",
|
".config",
|
||||||
"Application Support",
|
|
||||||
"Code",
|
"Code",
|
||||||
"User",
|
"User",
|
||||||
"mcp.json"
|
"mcp.json"
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
// Use Application Support for a stable, user-writable location
|
// Use Application Support for a stable, user-writable location
|
||||||
return Path.Combine(
|
return Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
"UnityMCP"
|
RootFolder
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Exception("Unsupported operating system.");
|
throw new Exception("Unsupported operating system.");
|
||||||
|
|
@ -126,6 +126,7 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
|
return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };
|
||||||
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
|
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(destinationDir);
|
Directory.CreateDirectory(destinationDir);
|
||||||
|
|
@ -140,8 +141,15 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
foreach (string dirPath in Directory.GetDirectories(sourceDir))
|
foreach (string dirPath in Directory.GetDirectories(sourceDir))
|
||||||
{
|
{
|
||||||
string dirName = Path.GetFileName(dirPath);
|
string dirName = Path.GetFileName(dirPath);
|
||||||
|
foreach (var skip in _skipDirs)
|
||||||
|
{
|
||||||
|
if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase))
|
||||||
|
goto NextDir;
|
||||||
|
}
|
||||||
|
try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { }
|
||||||
string destSubDir = Path.Combine(destinationDir, dirName);
|
string destSubDir = Path.Combine(destinationDir, dirName);
|
||||||
CopyDirectoryRecursive(dirPath, destSubDir);
|
CopyDirectoryRecursive(dirPath, destSubDir);
|
||||||
|
NextDir: ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,10 +309,11 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
candidates = new[]
|
candidates = new[]
|
||||||
{
|
{
|
||||||
// Preferred: WinGet Links shims (stable entrypoints)
|
// Preferred: WinGet Links shims (stable entrypoints)
|
||||||
|
// Per-user shim, then machine-wide shim
|
||||||
Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
|
Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
|
||||||
Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
|
|
||||||
// Optional low-priority fallback for atypical images
|
|
||||||
Path.Combine(programData, "Microsoft", "WinGet", "Links", "uv.exe"),
|
Path.Combine(programData, "Microsoft", "WinGet", "Links", "uv.exe"),
|
||||||
|
// ProgramFiles Links is uncommon; keep as low-priority fallback
|
||||||
|
Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
|
||||||
|
|
||||||
// Common per-user installs
|
// Common per-user installs
|
||||||
Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
|
Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
|
||||||
|
|
|
||||||
|
|
@ -395,22 +395,80 @@ namespace UnityMcpBridge.Editor
|
||||||
using (client)
|
using (client)
|
||||||
using (NetworkStream stream = client.GetStream())
|
using (NetworkStream stream = client.GetStream())
|
||||||
{
|
{
|
||||||
|
const int MaxMessageBytes = 64 * 1024 * 1024; // 64 MB safety cap
|
||||||
byte[] buffer = new byte[8192];
|
byte[] buffer = new byte[8192];
|
||||||
while (isRunning)
|
while (isRunning)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
// Read message with optional length prefix (8-byte big-endian)
|
||||||
if (bytesRead == 0)
|
bool usedFraming = false;
|
||||||
|
string commandText = null;
|
||||||
|
|
||||||
|
// First, attempt to read an 8-byte header
|
||||||
|
byte[] header = new byte[8];
|
||||||
|
int headerFilled = 0;
|
||||||
|
while (headerFilled < 8)
|
||||||
{
|
{
|
||||||
break; // Client disconnected
|
int r = await stream.ReadAsync(header, headerFilled, 8 - headerFilled);
|
||||||
|
if (r == 0)
|
||||||
|
{
|
||||||
|
// Disconnected
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
headerFilled += r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpret header as big-endian payload length, with plausibility check
|
||||||
|
ulong payloadLen = ReadUInt64BigEndian(header);
|
||||||
|
if (payloadLen > 0 && payloadLen <= (ulong)MaxMessageBytes)
|
||||||
|
{
|
||||||
|
// Framed message path
|
||||||
|
usedFraming = true;
|
||||||
|
byte[] payload = await ReadExactAsync(stream, (int)payloadLen);
|
||||||
|
commandText = System.Text.Encoding.UTF8.GetString(payload);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Legacy path: treat header bytes as the beginning of a JSON/plain message and read until we have a full JSON
|
||||||
|
usedFraming = false;
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
ms.Write(header, 0, header.Length);
|
||||||
|
|
||||||
|
// Read available data in chunks; stop when we have valid JSON or ping, or when no more data available for now
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// If we already have enough text, try to interpret
|
||||||
|
string currentText = System.Text.Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
string trimmed = currentText.Trim();
|
||||||
|
if (trimmed == "ping")
|
||||||
|
{
|
||||||
|
commandText = trimmed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (IsValidJson(trimmed))
|
||||||
|
{
|
||||||
|
commandText = trimmed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read next chunk
|
||||||
|
int r = await stream.ReadAsync(buffer, 0, buffer.Length);
|
||||||
|
if (r == 0)
|
||||||
|
{
|
||||||
|
// Disconnected mid-message; fall back to whatever we have
|
||||||
|
commandText = currentText;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ms.Write(buffer, 0, r);
|
||||||
|
|
||||||
|
if (ms.Length > MaxMessageBytes)
|
||||||
|
{
|
||||||
|
throw new IOException($"Incoming message exceeded {MaxMessageBytes} bytes cap");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string commandText = System.Text.Encoding.UTF8.GetString(
|
|
||||||
buffer,
|
|
||||||
0,
|
|
||||||
bytesRead
|
|
||||||
);
|
|
||||||
string commandId = Guid.NewGuid().ToString();
|
string commandId = Guid.NewGuid().ToString();
|
||||||
TaskCompletionSource<string> tcs = new();
|
TaskCompletionSource<string> tcs = new();
|
||||||
|
|
||||||
|
|
@ -422,6 +480,14 @@ namespace UnityMcpBridge.Editor
|
||||||
/*lang=json,strict*/
|
/*lang=json,strict*/
|
||||||
"{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}"
|
"{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (usedFraming)
|
||||||
|
{
|
||||||
|
// Mirror framing for response
|
||||||
|
byte[] outHeader = new byte[8];
|
||||||
|
WriteUInt64BigEndian(outHeader, (ulong)pingResponseBytes.Length);
|
||||||
|
await stream.WriteAsync(outHeader, 0, outHeader.Length);
|
||||||
|
}
|
||||||
await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length);
|
await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -433,6 +499,12 @@ namespace UnityMcpBridge.Editor
|
||||||
|
|
||||||
string response = await tcs.Task;
|
string response = await tcs.Task;
|
||||||
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
|
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
|
||||||
|
if (usedFraming)
|
||||||
|
{
|
||||||
|
byte[] outHeader = new byte[8];
|
||||||
|
WriteUInt64BigEndian(outHeader, (ulong)responseBytes.Length);
|
||||||
|
await stream.WriteAsync(outHeader, 0, outHeader.Length);
|
||||||
|
}
|
||||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -444,6 +516,55 @@ namespace UnityMcpBridge.Editor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read exactly count bytes or throw if stream closes prematurely
|
||||||
|
private static async Task<byte[]> ReadExactAsync(NetworkStream stream, int count)
|
||||||
|
{
|
||||||
|
byte[] data = new byte[count];
|
||||||
|
int offset = 0;
|
||||||
|
while (offset < count)
|
||||||
|
{
|
||||||
|
int r = await stream.ReadAsync(data, offset, count - offset);
|
||||||
|
if (r == 0)
|
||||||
|
{
|
||||||
|
throw new IOException("Connection closed before reading expected bytes");
|
||||||
|
}
|
||||||
|
offset += r;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ReadUInt64BigEndian(byte[] buffer)
|
||||||
|
{
|
||||||
|
if (buffer == null || buffer.Length < 8)
|
||||||
|
{
|
||||||
|
return 0UL;
|
||||||
|
}
|
||||||
|
return ((ulong)buffer[0] << 56)
|
||||||
|
| ((ulong)buffer[1] << 48)
|
||||||
|
| ((ulong)buffer[2] << 40)
|
||||||
|
| ((ulong)buffer[3] << 32)
|
||||||
|
| ((ulong)buffer[4] << 24)
|
||||||
|
| ((ulong)buffer[5] << 16)
|
||||||
|
| ((ulong)buffer[6] << 8)
|
||||||
|
| buffer[7];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteUInt64BigEndian(byte[] dest, ulong value)
|
||||||
|
{
|
||||||
|
if (dest == null || dest.Length < 8)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Destination buffer too small for UInt64");
|
||||||
|
}
|
||||||
|
dest[0] = (byte)(value >> 56);
|
||||||
|
dest[1] = (byte)(value >> 48);
|
||||||
|
dest[2] = (byte)(value >> 40);
|
||||||
|
dest[3] = (byte)(value >> 32);
|
||||||
|
dest[4] = (byte)(value >> 24);
|
||||||
|
dest[5] = (byte)(value >> 16);
|
||||||
|
dest[6] = (byte)(value >> 8);
|
||||||
|
dest[7] = (byte)(value);
|
||||||
|
}
|
||||||
|
|
||||||
private static void ProcessCommands()
|
private static void ProcessCommands()
|
||||||
{
|
{
|
||||||
List<string> processedIds = new();
|
List<string> processedIds = new();
|
||||||
|
|
|
||||||
|
|
@ -630,15 +630,29 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
if (unity == null) return false;
|
if (unity == null) return false;
|
||||||
var args = unity.args;
|
var args = unity.args;
|
||||||
if (args == null) return false;
|
if (args == null) return false;
|
||||||
foreach (var a in args)
|
// Prefer exact extraction of the --directory value and compare normalized paths
|
||||||
{
|
string[] strArgs = ((System.Collections.Generic.IEnumerable<object>)args)
|
||||||
string s = (string)a;
|
.Select(x => x?.ToString() ?? string.Empty)
|
||||||
if (!string.IsNullOrEmpty(s) && s.Contains(pythonDir, StringComparison.Ordinal))
|
.ToArray();
|
||||||
{
|
string dir = ExtractDirectoryArg(strArgs);
|
||||||
return true;
|
if (string.IsNullOrEmpty(dir)) return false;
|
||||||
|
return PathsEqual(dir, pythonDir);
|
||||||
}
|
}
|
||||||
|
catch { return false; }
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
private static bool PathsEqual(string a, string b)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string na = System.IO.Path.GetFullPath(a.Trim());
|
||||||
|
string nb = System.IO.Path.GetFullPath(b.Trim());
|
||||||
|
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
return string.Equals(na, nb, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
@ -883,7 +897,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
unityMCP = new
|
unityMCP = new
|
||||||
{
|
{
|
||||||
command = uvPath,
|
command = uvPath,
|
||||||
args = new[] { "--directory", pythonDir, "run", "server.py" }
|
args = new[] { "run", "--directory", pythonDir, "server.py" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -938,6 +952,30 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
&& System.IO.File.Exists(path);
|
&& System.IO.File.Exists(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ValidateUvBinarySafe(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false;
|
||||||
|
var psi = new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = path,
|
||||||
|
Arguments = "--version",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
using var p = System.Diagnostics.Process.Start(psi);
|
||||||
|
if (p == null) return false;
|
||||||
|
if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
|
||||||
|
if (p.ExitCode != 0) return false;
|
||||||
|
string output = p.StandardOutput.ReadToEnd().Trim();
|
||||||
|
return output.StartsWith("uv ");
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
private static string ExtractDirectoryArg(string[] args)
|
private static string ExtractDirectoryArg(string[] args)
|
||||||
{
|
{
|
||||||
if (args == null) return null;
|
if (args == null) return null;
|
||||||
|
|
@ -1026,7 +1064,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
// 1) Start from existing, only fill gaps
|
// 1) Start from existing, only fill gaps
|
||||||
string uvPath = IsValidUv(existingCommand) ? existingCommand : FindUvPath();
|
string uvPath = (ValidateUvBinarySafe(existingCommand) ? existingCommand : FindUvPath());
|
||||||
if (uvPath == null) return "UV package manager not found. Please install UV first.";
|
if (uvPath == null) return "UV package manager not found. Please install UV first.";
|
||||||
|
|
||||||
string serverSrc = ExtractDirectoryArg(existingArgs);
|
string serverSrc = ExtractDirectoryArg(existingArgs);
|
||||||
|
|
@ -1074,7 +1112,12 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
}
|
}
|
||||||
|
|
||||||
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
||||||
File.WriteAllText(configPath, mergedJson);
|
string tmp = configPath + ".tmp";
|
||||||
|
System.IO.File.WriteAllText(tmp, mergedJson, System.Text.Encoding.UTF8);
|
||||||
|
if (System.IO.File.Exists(configPath))
|
||||||
|
System.IO.File.Replace(tmp, configPath, null);
|
||||||
|
else
|
||||||
|
System.IO.File.Move(tmp, configPath);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("UnityMCP.UvPath", uvPath);
|
if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("UnityMCP.UvPath", uvPath);
|
||||||
|
|
@ -1124,7 +1167,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
unityMCP = new
|
unityMCP = new
|
||||||
{
|
{
|
||||||
command = uvPathManual,
|
command = uvPathManual,
|
||||||
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
args = new[] { "run", "--directory", pythonDir, "server.py" },
|
||||||
type = "stdio"
|
type = "stdio"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1148,7 +1191,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
unityMCP = new McpConfigServer
|
unityMCP = new McpConfigServer
|
||||||
{
|
{
|
||||||
command = uvPath,
|
command = uvPath,
|
||||||
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
args = new[] { "run", "--directory", pythonDir, "server.py" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1368,7 +1411,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
unityMCP = new McpConfigServer
|
unityMCP = new McpConfigServer
|
||||||
{
|
{
|
||||||
command = uvPath,
|
command = uvPath,
|
||||||
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
args = new[] { "run", "--directory", pythonDir, "server.py" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import errno
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from config import config
|
from config import config
|
||||||
from port_discovery import PortDiscovery
|
from port_discovery import PortDiscovery
|
||||||
|
import struct
|
||||||
|
|
||||||
# Configure logging using settings from config
|
# Configure logging using settings from config
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|
@ -53,60 +54,52 @@ class UnityConnection:
|
||||||
finally:
|
finally:
|
||||||
self.sock = None
|
self.sock = None
|
||||||
|
|
||||||
def receive_full_response(self, sock, buffer_size=config.buffer_size) -> bytes:
|
def receive_full_response(self, sock) -> bytes:
|
||||||
"""Receive a complete response from Unity, handling chunked data."""
|
"""Receive a complete response from Unity using 8-byte length-prefixed framing, with legacy fallback."""
|
||||||
chunks = []
|
sock.settimeout(config.connection_timeout)
|
||||||
sock.settimeout(config.connection_timeout) # Use timeout from config
|
# Try framed first
|
||||||
try:
|
try:
|
||||||
|
header = self._read_exact(sock, 8)
|
||||||
|
(payload_len,) = struct.unpack('>Q', header)
|
||||||
|
if 0 < payload_len <= (64 * 1024 * 1024):
|
||||||
|
return self._read_exact(sock, payload_len)
|
||||||
|
# Implausible length -> treat as legacy stream; fall through
|
||||||
|
legacy_prefix = header
|
||||||
|
except Exception:
|
||||||
|
# Could not read header — treat as legacy
|
||||||
|
legacy_prefix = b''
|
||||||
|
|
||||||
|
# Legacy: read until parses as JSON or times out
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
if legacy_prefix:
|
||||||
|
chunks.append(legacy_prefix)
|
||||||
while True:
|
while True:
|
||||||
chunk = sock.recv(buffer_size)
|
chunk = sock.recv(config.buffer_size)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
if not chunks:
|
|
||||||
raise Exception("Connection closed before receiving data")
|
|
||||||
break
|
|
||||||
chunks.append(chunk)
|
|
||||||
|
|
||||||
# Process the data received so far
|
|
||||||
data = b''.join(chunks)
|
data = b''.join(chunks)
|
||||||
decoded_data = data.decode('utf-8')
|
if not data:
|
||||||
|
raise Exception("Connection closed before receiving data")
|
||||||
# Check if we've received a complete response
|
return data
|
||||||
|
chunks.append(chunk)
|
||||||
|
data = b''.join(chunks)
|
||||||
try:
|
try:
|
||||||
# Special case for ping-pong
|
if data.strip() == b'ping':
|
||||||
if decoded_data.strip().startswith('{"status":"success","result":{"message":"pong"'):
|
|
||||||
logger.debug("Received ping response")
|
|
||||||
return data
|
return data
|
||||||
|
json.loads(data.decode('utf-8'))
|
||||||
# Handle escaped quotes in the content
|
|
||||||
if '"content":' in decoded_data:
|
|
||||||
# Find the content field and its value
|
|
||||||
content_start = decoded_data.find('"content":') + 9
|
|
||||||
content_end = decoded_data.rfind('"', content_start)
|
|
||||||
if content_end > content_start:
|
|
||||||
# Replace escaped quotes in content with regular quotes
|
|
||||||
content = decoded_data[content_start:content_end]
|
|
||||||
content = content.replace('\\"', '"')
|
|
||||||
decoded_data = decoded_data[:content_start] + content + decoded_data[content_end:]
|
|
||||||
|
|
||||||
# Validate JSON format
|
|
||||||
json.loads(decoded_data)
|
|
||||||
|
|
||||||
# If we get here, we have valid JSON
|
|
||||||
logger.info(f"Received complete response ({len(data)} bytes)")
|
|
||||||
return data
|
return data
|
||||||
except json.JSONDecodeError:
|
except Exception:
|
||||||
# We haven't received a complete valid JSON response yet
|
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error processing response chunk: {str(e)}")
|
def _read_exact(self, sock: socket.socket, n: int) -> bytes:
|
||||||
# Continue reading more chunks as this might not be the complete response
|
buf = bytearray(n)
|
||||||
continue
|
view = memoryview(buf)
|
||||||
except socket.timeout:
|
read = 0
|
||||||
logger.warning("Socket timeout during receive")
|
while read < n:
|
||||||
raise Exception("Timeout receiving Unity response")
|
r = sock.recv_into(view[read:])
|
||||||
except Exception as e:
|
if r == 0:
|
||||||
logger.error(f"Error during receive: {str(e)}")
|
raise Exception("Connection closed during read")
|
||||||
raise
|
read += r
|
||||||
|
return bytes(buf)
|
||||||
|
|
||||||
def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||||
"""Send a command with retry/backoff and port rediscovery. Pings only when requested."""
|
"""Send a command with retry/backoff and port rediscovery. Pings only when requested."""
|
||||||
|
|
@ -160,13 +153,14 @@ class UnityConnection:
|
||||||
|
|
||||||
# Build payload
|
# Build payload
|
||||||
if command_type == 'ping':
|
if command_type == 'ping':
|
||||||
payload = b'ping'
|
body = b'ping'
|
||||||
else:
|
else:
|
||||||
command = {"type": command_type, "params": params or {}}
|
command = {"type": command_type, "params": params or {}}
|
||||||
payload = json.dumps(command, ensure_ascii=False).encode('utf-8')
|
body = json.dumps(command, ensure_ascii=False).encode('utf-8')
|
||||||
|
|
||||||
# Send
|
# Send with 8-byte big-endian length prefix for robustness
|
||||||
self.sock.sendall(payload)
|
header = struct.pack('>Q', len(body))
|
||||||
|
self.sock.sendall(header + body)
|
||||||
|
|
||||||
# During retry bursts use a short receive timeout
|
# During retry bursts use a short receive timeout
|
||||||
if attempt > 0 and last_short_timeout is None:
|
if attempt > 0 and last_short_timeout is None:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
version = 1
|
version = 1
|
||||||
revision = 1
|
revision = 1
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
|
|
@ -16,6 +16,7 @@ name = "anyio"
|
||||||
version = "4.9.0"
|
version = "4.9.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "sniffio" },
|
{ name = "sniffio" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
|
@ -55,6 +56,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
@ -179,6 +192,33 @@ dependencies = [
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
|
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
|
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
|
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
|
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
|
||||||
|
|
@ -207,6 +247,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
|
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
|
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
|
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -247,6 +296,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "markdown-it-py" },
|
{ name = "markdown-it-py" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
|
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|
@ -342,6 +392,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "h11" },
|
{ name = "h11" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
|
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
### macOS: Claude CLI fails to start (dyld ICU library not loaded)
|
||||||
|
|
||||||
|
- Symptoms
|
||||||
|
- Unity MCP error: “Failed to start Claude CLI. dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.71.dylib …”
|
||||||
|
- Running `claude` in Terminal fails with missing `libicui18n.xx.dylib`.
|
||||||
|
|
||||||
|
- Cause
|
||||||
|
- Homebrew Node (or the `claude` binary) was linked against an ICU version that’s no longer installed; dyld can’t find that dylib.
|
||||||
|
|
||||||
|
- Fix options (pick one)
|
||||||
|
- Reinstall Homebrew Node (relinks to current ICU), then reinstall CLI:
|
||||||
|
```bash
|
||||||
|
brew update
|
||||||
|
brew reinstall node
|
||||||
|
npm uninstall -g @anthropic-ai/claude-code
|
||||||
|
npm install -g @anthropic-ai/claude-code
|
||||||
|
```
|
||||||
|
- Use NVM Node (avoids Homebrew ICU churn):
|
||||||
|
```bash
|
||||||
|
nvm install --lts
|
||||||
|
nvm use --lts
|
||||||
|
npm install -g @anthropic-ai/claude-code
|
||||||
|
# Unity MCP → Claude Code → Choose Claude Location → ~/.nvm/versions/node/<ver>/bin/claude
|
||||||
|
```
|
||||||
|
- Use the native installer (puts claude in a stable path):
|
||||||
|
```bash
|
||||||
|
# macOS/Linux
|
||||||
|
curl -fsSL https://claude.ai/install.sh | bash
|
||||||
|
# Unity MCP → Claude Code → Choose Claude Location → /opt/homebrew/bin/claude or ~/.local/bin/claude
|
||||||
|
```
|
||||||
|
|
||||||
|
- After fixing
|
||||||
|
- In Unity MCP (Claude Code), click “Choose Claude Location” and select the working `claude` binary, then Register again.
|
||||||
|
|
||||||
|
- More details
|
||||||
|
- See: Troubleshooting Unity MCP and Claude Code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### FAQ (Claude Code)
|
||||||
|
|
||||||
|
- Q: Unity can’t find `claude` even though Terminal can.
|
||||||
|
- A: macOS apps launched from Finder/Hub don’t inherit your shell PATH. In the Unity MCP window, click “Choose Claude Location” and select the absolute path (e.g., `/opt/homebrew/bin/claude` or `~/.nvm/versions/node/<ver>/bin/claude`).
|
||||||
|
|
||||||
|
- Q: I installed via NVM; where is `claude`?
|
||||||
|
- A: Typically `~/.nvm/versions/node/<ver>/bin/claude`. Our UI also scans NVM versions and you can browse to it via “Choose Claude Location”.
|
||||||
|
|
||||||
|
- Q: The Register button says “Claude Not Found”.
|
||||||
|
- A: Install the CLI or set the path. Click the orange “[HELP]” link in the Unity MCP window for step‑by‑step install instructions, then choose the binary location.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "unity-mcp",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
Loading…
Reference in New Issue