Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable public changes to Nexus Unity are documented here.
- Relicensed Nexus Unity from `GPL-3.0-only` to the `MIT` license to remove copyleft friction for commercial Unity studios. All prior contributors consented to the relicense.

### Fixed
- Updated Claude Code and Gemini CLI setup commands for their current option ordering and explicit project-scoped stdio registration; Gemini setup no longer bypasses tool approvals with `--trust`.
- Antigravity setup now writes its workspace configuration to `.agents/mcp_config.json` instead of the legacy global Gemini config path.
- Cline setup now writes its shared MCP settings to `~/.cline/data/settings/cline_mcp_settings.json` instead of the retired VS Code extension storage path.
- Claude Code setup now skips the CLI add command when removal of a stale project registration fails, logs the actionable failure, and falls back directly to `.mcp.json`.
- The local HTTP/WebSocket control plane now requires a per-session auth token before JSON-RPC dispatch; generated MCP configs pass the token through the Python bridge.
- `add_component` now returns a clear `GameObject not found` error for stale instance IDs instead of throwing a raw Unity null reference.
- `batch_execute` now caps batches at 50 requests and rejects nested batch execution.
Expand Down
4 changes: 3 additions & 1 deletion DOCUMENTATION.MD
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ Console logging defaults to `Important`, which keeps the Unity Console focused o

The `Integrations` tab builds all snippets from the resolved Python interpreter and the project-root bridge path `nexus_unity_bridge.py`. Auto setup deploys the bridge first. JSON-style clients use `mcpServers.nexus-unity`, VS Code-compatible clients use `.vscode/mcp.json` with `servers.nexus-unity`, Claude Code uses a project-root `.mcp.json` with `mcpServers.nexus-unity`, and Codex uses `[mcp_servers.nexus-unity]` in TOML.

Claude Code and Claude Desktop are distinct products with distinct config locations. The Claude Code card registers `nexus-unity` in `<projectRoot>/.mcp.json` (via the `claude` CLI when it is on `PATH`, otherwise by writing the file directly); the Claude Desktop card writes the desktop app config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows). Claude Code does not read the desktop config.
Claude Code and Claude Desktop are distinct products with distinct config locations. The Claude Code card registers `nexus-unity` in `<projectRoot>/.mcp.json` (via the `claude` CLI when it is on `PATH`, otherwise by writing the file directly). If the CLI cannot remove a stale registration, setup skips its add command and writes `.mcp.json` directly instead; the CLI error is logged for diagnosis. The Claude Desktop card writes the desktop app config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows). Claude Code does not read the desktop config.

Gemini CLI uses a project-scoped stdio registration in `.gemini/settings.json`; Auto Setup keeps Gemini's normal tool-approval prompts enabled. Antigravity uses the workspace-local `.agents/mcp_config.json` with the standard `mcpServers` object, not the older shared Gemini configuration path. Cline uses `~/.cline/data/settings/cline_mcp_settings.json` for shared MCP settings.

### Platform notes

Expand Down
21 changes: 16 additions & 5 deletions Editor/MCPCliInstaller.ClaudeCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,29 @@
}
}

private static void ExecuteClaudeCodeLinkSequence(string scriptPath, string pythonPath)

Check warning on line 31 in Editor/MCPCliInstaller.ClaudeCode.cs

View workflow job for this annotation

GitHub Actions / Static validation

NQG012 ExecuteClaudeCodeLinkSequence

Method has 64 lines; consider splitting before it exceeds 150.

Check warning on line 31 in Editor/MCPCliInstaller.ClaudeCode.cs

View workflow job for this annotation

GitHub Actions / Documentation quality AI

NQG012 ExecuteClaudeCodeLinkSequence

Method has 64 lines; consider splitting before it exceeds 150.
{
string claudePath = ResolveExecutablePath("claude");

// Preferred path: the official claude CLI writes the project-scoped .mcp.json for us.
if (!string.IsNullOrEmpty(claudePath) && claudePath != "claude")
{
// 1. Remove any stale registration so re-running is idempotent (silent).
RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "remove", "nexus-unity", "-s", "project"), claudePath, false, "Claude Code");
// 1. Remove any stale registration so re-running is idempotent.
// A missing registration is expected on first setup and is safe to add over.
bool removedStaleRegistration = RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "remove", "--scope", "project", "nexus-unity"), claudePath, false, "Claude Code", out string removeError, false);
bool registrationWasAbsent = !removedStaleRegistration && IsClaudeCodeRegistrationAbsent(removeError);

// 2. Add the server at project scope (silent, because we have a file fallback).
if (RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "add", "nexus-unity", "-s", "project", "-e", MCPServer.AuthTokenEnvironmentVariable + "=" + MCPServer.AuthToken, "--", pythonPath, scriptPath), claudePath, false, "Claude Code"))
// 2. Add the server at project scope only when the prior state is known to be clear.
if ((removedStaleRegistration || registrationWasAbsent) && RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "add", "--transport", "stdio", "--scope", "project", "--env", MCPServer.AuthTokenEnvironmentVariable + "=" + MCPServer.AuthToken, "nexus-unity", "--", pythonPath, scriptPath), claudePath, false, "Claude Code"))
{
NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Successfully linked Nexus Unity to Claude Code via '" + claudePath + "'.", true);
EditorUtility.DisplayDialog("MCP Success", "Successfully linked Nexus Unity to Claude Code.\n\nRun /mcp inside Claude Code (or restart it) to load the server.", "OK");
return;
}

NexusEditorLog.Warning(NexusLogCategory.Integrations, "[MCP] Claude Code CLI command failed at '" + claudePath + "'. Falling back to direct .mcp.json edit.");
NexusEditorLog.Warning(NexusLogCategory.Integrations, (removedStaleRegistration || registrationWasAbsent)
? "[MCP] Claude Code CLI add command failed at '" + claudePath + "'. Falling back to direct .mcp.json edit."
: "[MCP] Claude Code CLI could not remove the existing registration at '" + claudePath + "': " + removeError + ". Skipping CLI add and falling back to direct .mcp.json edit.");
}

// Fallback: write the project-root .mcp.json directly.
Expand Down Expand Up @@ -88,5 +92,12 @@
EditorUtility.DisplayDialog("MCP Error", "Failed to write .mcp.json for Claude Code.\n\n" + e.Message, "OK");
}
}

internal static bool IsClaudeCodeRegistrationAbsent(string error)
{
return !string.IsNullOrEmpty(error)
&& error.IndexOf("nexus-unity", StringComparison.OrdinalIgnoreCase) >= 0
&& error.IndexOf("not found", StringComparison.OrdinalIgnoreCase) >= 0;
}
}
}
8 changes: 4 additions & 4 deletions Editor/MCPCliInstaller.Gemini.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ private static void ExecuteGeminiLinkSequence(string scriptPath)
string geminiPath = ResolveExecutablePath("gemini");
string pythonPath = ResolvePythonPath();

// 1. Ensure clean slate by removing existing registration
RunInstallerProcess(CreateProcessStartInfo(geminiPath, "mcp", "remove", "nexus-unity"), geminiPath, false, "Gemini");
// 1. Ensure clean slate by removing the existing project registration.
RunInstallerProcess(CreateProcessStartInfo(geminiPath, "mcp", "remove", "--scope", "project", "nexus-unity"), geminiPath, false, "Gemini");

// 2. Add new registration with stable path
RunInstallerProcess(CreateProcessStartInfo(geminiPath, "mcp", "add", "nexus-unity", "--trust", "-e", MCPServer.AuthTokenEnvironmentVariable + "=" + MCPServer.AuthToken, pythonPath, scriptPath), geminiPath, true, "Gemini");
// 2. Add a stdio registration without bypassing Gemini's tool-approval prompts.
RunInstallerProcess(CreateProcessStartInfo(geminiPath, "mcp", "add", "--scope", "project", "--transport", "stdio", "--env", MCPServer.AuthTokenEnvironmentVariable + "=" + MCPServer.AuthToken, "nexus-unity", pythonPath, scriptPath), geminiPath, true, "Gemini");
}
}
}
11 changes: 9 additions & 2 deletions Editor/MCPCliInstaller.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using UnityEditor;

Check warning on line 1 in Editor/MCPCliInstaller.cs

View workflow job for this annotation

GitHub Actions / Static validation

NQG002

File has 438 lines; consider splitting before it exceeds 450.

Check warning on line 1 in Editor/MCPCliInstaller.cs

View workflow job for this annotation

GitHub Actions / Documentation quality AI

NQG002

File has 438 lines; consider splitting before it exceeds 450.
using UnityEngine;
using System.IO;
using System.Diagnostics;
Expand Down Expand Up @@ -163,7 +163,7 @@
return null;
}

private static string ResolveExecutablePath(string name)

Check warning on line 166 in Editor/MCPCliInstaller.cs

View workflow job for this annotation

GitHub Actions / Static validation

NQG012 ResolveExecutablePath

Method has 52 lines; consider splitting before it exceeds 150.

Check warning on line 166 in Editor/MCPCliInstaller.cs

View workflow job for this annotation

GitHub Actions / Documentation quality AI

NQG012 ResolveExecutablePath

Method has 52 lines; consider splitting before it exceeds 150.
{
if (Application.platform == RuntimePlatform.WindowsEditor)
{
Expand Down Expand Up @@ -397,11 +397,17 @@

private static bool RunInstallerProcess(ProcessStartInfo psi, string cliPath, bool showSuccessDialog, string cliName)
{
return RunInstallerProcess(psi, cliPath, showSuccessDialog, cliName, out _, true);
}

private static bool RunInstallerProcess(ProcessStartInfo psi, string cliPath, bool showSuccessDialog, string cliName, out string error, bool logFailure)
{
error = string.Empty;
try
{
using (Process p = Process.Start(psi))
{
string error = p.StandardError.ReadToEnd();
error = p.StandardError.ReadToEnd();
p.WaitForExit();

if (p.ExitCode == 0)
Expand All @@ -411,7 +417,7 @@
else
{
string msg = "CLI command failed at " + cliPath + ".\n\nExit Code: " + p.ExitCode + "\nError: " + error;
NexusEditorLog.Warning(NexusLogCategory.Integrations, "[MCP] " + msg);
if (logFailure) NexusEditorLog.Warning(NexusLogCategory.Integrations, "[MCP] " + msg);

if (showSuccessDialog) // If this was supposed to be the final step
{
Expand All @@ -422,6 +428,7 @@
}
catch (Exception e)
{
error = e.Message;
NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Process start failed: " + e.Message);
}
return false;
Expand Down
12 changes: 3 additions & 9 deletions Editor/NexusMcpConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ internal static List<NexusMcpClientInfo> BuildAll(string bridgePath, string pyth
// Plain JSON-config clients - one row per tool, no client-specific behavior beyond path/format.
var jsonClients = new[]
{
(NexusMcpClientKind.Antigravity, "Antigravity", Path.Combine(homeDir, ".gemini", "config", "mcp_config.json"),
"Paste into ~/.gemini/config/mcp_config.json (shared with Gemini), then restart Antigravity sessions.", "mcpServers"),
(NexusMcpClientKind.Antigravity, "Antigravity", Path.Combine(projectRoot, ".agents", "mcp_config.json"),
"Paste into .agents/mcp_config.json, then restart or reload Antigravity sessions.", "mcpServers"),
(NexusMcpClientKind.Cursor, "Cursor", Path.Combine(projectRoot, ".cursor", "mcp.json"),
"Paste into .cursor/mcp.json or use Auto Setup for this Unity project.", "mcpServers"),
(NexusMcpClientKind.VsCode, "VS Code", Path.Combine(projectRoot, ".vscode", "mcp.json"),
Expand Down Expand Up @@ -433,13 +433,7 @@ private static string GetClaudeDesktopConfigPath()
// not in the workspace - same layout on macOS/Windows/Linux, rooted at the OS config dir for VS Code.
private static string GetClineConfigPath(string homeDir)
{
string vsCodeUserDir = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? Path.Combine(homeDir, "Library", "Application Support", "Code", "User")
: RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.Combine(homeDir, "AppData", "Roaming", "Code", "User")
: Path.Combine(homeDir, ".config", "Code", "User");

return Path.Combine(vsCodeUserDir, "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json");
return Path.Combine(homeDir, ".cline", "data", "settings", "cline_mcp_settings.json");
}
}
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ The Unity window can also deploy the bridge to the project root for CLIs that pr

For Codex, Claude Desktop, Claude Code, Gemini, Antigravity, Cursor, VS Code/Cline/Roo, Windsurf, or compatible MCP clients, open `Window > Nexus Unity` and use the `Integrations` tab.

> **Claude Code vs. Claude Desktop:** these are different products with different config locations. The `Claude Code` card registers the `nexus-unity` server in a project-scoped `.mcp.json` at the Unity project root (using the `claude` CLI when it is on `PATH`, otherwise writing the file directly). The `Claude Desktop` card writes the desktop app's `claude_desktop_config.json`. After running `Auto Setup` for Claude Code, run `/mcp` inside Claude Code (or restart it) to load the server.
> **Claude Code vs. Claude Desktop:** these are different products with different config locations. The `Claude Code` card registers the `nexus-unity` server in a project-scoped `.mcp.json` at the Unity project root (using the `claude` CLI when it is on `PATH`, otherwise writing the file directly). If the CLI cannot remove an existing registration, setup skips its add command and writes `.mcp.json` directly instead. The `Claude Desktop` card writes the desktop app's `claude_desktop_config.json`. After running `Auto Setup` for Claude Code, run `/mcp` inside Claude Code (or restart it) to load the server.

Each integration card shows `Detected`, `Not found`, `Configured`, `Outdated`, or `Error` status and provides:

- `Auto Setup` when Nexus Unity can safely write or invoke the client configuration.
- `Copy Config` for manual setup.
- `Open Config` when the client uses a known config file path.

Nexus Unity generates configs from the resolved Python interpreter and the deployed `nexus_unity_bridge.py` path. User/global config writes create a timestamped backup before modifying an existing file.
Nexus Unity generates configs from the resolved Python interpreter and the deployed `nexus_unity_bridge.py` path. Antigravity uses the workspace-local `.agents/mcp_config.json`; its older global Gemini config location is not used. User/global config writes create a timestamped backup before modifying an existing file.

> **Windows / Linux:** The Python interpreter is resolved as `python3`, then `python`, then the Windows `py` launcher — make sure a Python 3 install is on `PATH`. CLI-based cards (Codex, Gemini, and the Claude Code CLI path) are detected with `where` on Windows and `which` on macOS/Linux, so the CLI must be on the `PATH` of the shell that launched Unity for `Auto Setup` to find it; otherwise use `Copy Config`.

Expand Down
7 changes: 7 additions & 0 deletions Tests~/Editor/MCPCliInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ namespace UnityMCP.Editor.Tests
{
public class MCPCliInstallerTests
{
[Test]
public void ClaudeCodeMissingRegistrationIsSafeToAdd()
{
Assert.IsTrue(MCPCliInstaller.IsClaudeCodeRegistrationAbsent("MCP server 'nexus-unity' not found."));
Assert.IsFalse(MCPCliInstaller.IsClaudeCodeRegistrationAbsent("Could not read project configuration."));
}

[Test]
public void CreateProcessStartInfoPreservesShellMetacharactersAsArguments()
{
Expand Down
37 changes: 37 additions & 0 deletions Tests~/Editor/NexusMcpConfigGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,43 @@ public void WriteVsCodeConfigUsesServersRoot()
}
}

[Test]
public void AntigravityUsesWorkspaceMcpConfig()
{
string root = CreateTempRoot();
try
{
string projectRoot = Path.Combine(root, "Project");
var antigravity = NexusMcpConfigGenerator.BuildAll("/bridge.py", "/python3", projectRoot, Path.Combine(root, "Home"))
.First(item => item.Kind == NexusMcpClientKind.Antigravity);

Assert.AreEqual(Path.Combine(projectRoot, ".agents", "mcp_config.json"), antigravity.ConfigPath);
Assert.AreEqual(NexusMcpConfigFormat.JsonMcpServers, antigravity.Format);
}
finally
{
DeleteTempRoot(root);
}
}

[Test]
public void ClineUsesCurrentSharedSettingsPath()
{
string root = CreateTempRoot();
try
{
string homeRoot = Path.Combine(root, "Home");
var cline = NexusMcpConfigGenerator.BuildAll("/bridge.py", "/python3", Path.Combine(root, "Project"), homeRoot)
.First(item => item.Kind == NexusMcpClientKind.Cline);

Assert.AreEqual(Path.Combine(homeRoot, ".cline", "data", "settings", "cline_mcp_settings.json"), cline.ConfigPath);
}
finally
{
DeleteTempRoot(root);
}
}

[Test]
public void DetectsConfiguredJsonServer()
{
Expand Down
Loading