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
12 changes: 6 additions & 6 deletions .code-linter.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"max_file_lines": 750,
"max_function_lines": 150,
"max_nesting_depth": 8,
"max_parameters": 8,
"max_comment_lines": 15,
"max_types_per_file": 6,
"max_file_lines": 300,
"max_function_lines": 50,
"max_nesting_depth": 4,
"max_parameters": 5,
"max_comment_lines": 5,
"max_types_per_file": 2,
"include_extensions": [
".cs",
".py"
Expand Down
10 changes: 10 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
* @daliys

Check warning on line 1 in .github/CODEOWNERS

View workflow job for this annotation

GitHub Actions / code-linter / Code Linter

coverage_gap

Unknown Text/Config is not mapped to a structural checker.

# Quality Gate & CI Policy files strictly owned by @daliys
/.code-linter.json @daliys
/.ruff.toml @daliys
/.unity-quality-gate.json @daliys
/.swift-quality-gate.json @daliys
/.slop-review.json @daliys
/.github/workflows/ @daliys
/.github/CODEOWNERS @daliys
2 changes: 1 addition & 1 deletion API_REFERENCE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ Actions: `undo`, `redo`, `play`, `pause`, `step`, `menu`, `read_logs`, `clear_lo
### `unity_ui_automation`
Actions: `list_windows`, `get_hierarchy`, `query`, `get_window_rect`, `set_window_rect`, `capture_window_snapshot`, `click`, `input`.

`get_hierarchy` accepts `deep`. `query` accepts `name`, `text`, and `class_name`. Window rect and snapshot actions are intended for resize/layout QA; window snapshots include rect and hierarchy, with best-effort PNG image capture on macOS.
`get_hierarchy` accepts `deep`, `max_depth`, and `max_elements` (with `children_truncated` and root `truncated` markers when capped). `query` accepts `name`, `text`, `class_name`, `max_depth`, and `max_results`. Window rect and snapshot actions are intended for resize/layout QA; window snapshots include rect and hierarchy (supporting `max_depth` and `max_elements`), with best-effort PNG image capture on macOS.

### `unity_wait`
Conditions: `compilation`, `play_mode`, `import`, `editor_idle`.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ All notable public changes to Nexus Unity are documented here.
- Consolidated duplicate internal Ollama-review and serialized-property write helpers.

### Fixed
- Add traversal depth and element/result bounds to UI Toolkit hierarchy serialization and element queries (`ui_get_hierarchy`, `ui_query_elements`, and `ui_capture_window_snapshot`), preventing editor stalls and payload bloat on complex UI windows, with `children_truncated` and `truncated` markers on partial hierarchy responses (#126).
- Dispatch WebSocket request handling asynchronously in `Task.Run`, ensuring `ServerLoop` remains non-blocking for concurrent HTTP JSON-RPC requests and new connections (#129).
- `EditorApplication.update` subscription for `HandlePostCompileFocusReturn` is now idempotent (unsubscribes before subscribing), preventing stacked duplicate frame handlers on repeated initialization or domain reloads (#138).
- `ListPlayerPrefs` now uses `ProcessStartInfo.ArgumentList` and timeout guards for macOS `defaults read` execution, preventing process hangs, alongside regex unhashing for Windows registry keys, Linux XML prefs support, and double-default type verification (#143).
Expand Down
2 changes: 1 addition & 1 deletion DOCUMENTATION.MD
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ MCP bridge:
- `unity_hierarchy_manager` can create primitives with name, parent, transform, and material path; it can also rename, set transforms, and pass through `create_hierarchy`.
- Raw `run_tests` returns `Submitted` when Unity accepts the asynchronous request; it does not confirm that execution has begun or completed. `unity_editor_controller` `run_tests_wait` waits in the Python bridge by polling raw `get_test_results` instead of blocking the Unity main thread.
- `unity_editor_controller` also exposes `get_tool_usage_stats` and `reset_tool_usage_stats` so diagnostics are not split between raw and manager workflows.
- `unity_ui_automation` passes through `deep` for UI hierarchy reads, `class_name` for queries, and window rect/snapshot actions for layout checks.
- `unity_ui_automation` passes through `deep`, `max_depth`, and `max_elements` for UI hierarchy reads, `class_name`, `max_depth`, and `max_results` for queries, and window rect/snapshot actions for layout checks.

## Current Compatibility Decisions

Expand Down
70 changes: 38 additions & 32 deletions Editor/MCPCliInstaller.ClaudeCode.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using UnityEditor;
using UnityEngine;
using System.IO;
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;

namespace UnityMCP.Editor
{
Expand All @@ -15,9 +15,7 @@ public static partial class MCPCliInstaller
/// <remarks>
/// Prefers the official <c>claude</c> CLI (<c>claude mcp add --scope project</c>, which itself writes
/// <c>.mcp.json</c>). When the CLI is unavailable or fails, it falls back to writing <c>.mcp.json</c> directly.
/// Either path produces the same project-root <c>.mcp.json</c> that the Integrations tab tracks. This is distinct
/// from the Claude Desktop app config, which is handled by the generic JSON-config codepath in
/// <see cref="NexusMcpConfigGenerator"/> instead of a dedicated CLI installer.
/// Either path produces the same project-root <c>.mcp.json</c> that the Integrations tab tracks.
/// </remarks>
public static void LinkToClaudeCode()
{
Expand All @@ -31,45 +29,43 @@ public static void LinkToClaudeCode()
private static void ExecuteClaudeCodeLinkSequence(string scriptPath, string pythonPath)
{
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.
// 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 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"))
if (TryLinkViaClaudeCli(claudePath, scriptPath, pythonPath))
{
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;
}
}

WriteClaudeCodeDirectJson(scriptPath, pythonPath);
}

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.");
private static bool TryLinkViaClaudeCli(string claudePath, string scriptPath, string pythonPath)
{
bool removedStale = RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "remove", "--scope", "project", "nexus-unity"), false, "Claude Code", out string removeError, false);
bool registrationAbsent = !removedStale && IsClaudeCodeRegistrationAbsent(removeError);

if ((removedStale || registrationAbsent) && 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 true;
}

// Fallback: write the project-root .mcp.json directly.
NexusEditorLog.Warning(NexusLogCategory.Integrations, (removedStale || registrationAbsent)
? "[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.");
return false;
}

private static void WriteClaudeCodeDirectJson(string scriptPath, string pythonPath)
{
try
{
string projectRoot = Path.GetDirectoryName(Application.dataPath);
string configPath = Path.Combine(projectRoot, ".mcp.json");

JObject config;
if (File.Exists(configPath))
{
try { config = JObject.Parse(File.ReadAllText(configPath)); }
catch { config = new JObject(); }
}
else
{
config = new JObject();
}

JObject config = LoadOrCreateJsonObject(configPath);
if (config["mcpServers"] == null) config["mcpServers"] = new JObject();
JObject servers = (JObject)config["mcpServers"];

Expand All @@ -93,6 +89,16 @@ private static void ExecuteClaudeCodeLinkSequence(string scriptPath, string pyth
}
}

private static JObject LoadOrCreateJsonObject(string path)
{
if (File.Exists(path))
{
try { return JObject.Parse(File.ReadAllText(path)); }
catch { return new JObject(); }
}
return new JObject();
}

internal static bool IsClaudeCodeRegistrationAbsent(string error)
{
return !string.IsNullOrEmpty(error)
Expand Down
164 changes: 164 additions & 0 deletions Editor/MCPCliInstaller.Deploy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System;
using System.IO;
using UnityEditor;
using UnityEngine;

namespace UnityMCP.Editor
{
public static partial class MCPCliInstaller
{
private static bool DeployBridgeScript(out string destinationPath)
{
destinationPath = null;
string sourcePath = FindBridgeScript();

if (string.IsNullOrEmpty(sourcePath))
{
NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Could not find 'nexus_unity_bridge.py' in the project.");
EditorUtility.DisplayDialog("MCP Error", "Could not find 'nexus_unity_bridge.py'.\n\nEnsure the library is correctly imported.", "OK");
return false;
}

string projectRoot = Path.GetDirectoryName(Application.dataPath);
destinationPath = Path.Combine(projectRoot, "nexus_unity_bridge.py");

try
{
File.Copy(sourcePath, destinationPath, true);
DeployBridgeModule(projectRoot, sourcePath);
NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge script deployed to stable location: " + destinationPath, true);
DeployDocumentationPointer(projectRoot, sourcePath);
return true;
}
catch (Exception e)
{
NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Failed to deploy bridge or docs: " + e.Message);
EditorUtility.DisplayDialog("MCP Error", "Failed to deploy integration files to project root.\n\n" + e.Message, "OK");
return false;
}
}

private static void DeployBridgeModule(string projectRoot, string sourcePath)
{
string sourceDir = Path.GetDirectoryName(sourcePath);
string sourceModuleDir = Path.Combine(sourceDir, "nexus_bridge");
if (!Directory.Exists(sourceModuleDir))
{
return;
}

string destinationModuleDir = Path.Combine(projectRoot, "nexus_bridge");
CopyDirectory(sourceModuleDir, destinationModuleDir);
NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge module deployed to stable location: " + destinationModuleDir);
}

private static void CopyDirectory(string sourceDir, string destinationDir)
{
Directory.CreateDirectory(destinationDir);

foreach (string file in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(file);
if (fileName.EndsWith(".meta", StringComparison.OrdinalIgnoreCase) ||
fileName.EndsWith(".pyc", StringComparison.OrdinalIgnoreCase))
{
continue;
}

File.Copy(file, Path.Combine(destinationDir, fileName), true);
}

foreach (string dir in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(dir);
if (dirName == "__pycache__")
{
continue;
}

CopyDirectory(dir, Path.Combine(destinationDir, dirName));
}
}

private static string FindLibraryRoot(string sourcePath)
{
string dir = Path.GetDirectoryName(sourcePath);
while (!string.IsNullOrEmpty(dir))
{
if (File.Exists(Path.Combine(dir, "package.json")))
{
return dir;
}
dir = Path.GetDirectoryName(dir);
}
return null;
}

private static void DeployDocumentationPointer(string projectRoot, string sourcePath)
{
string libraryRoot = FindLibraryRoot(sourcePath);
if (string.IsNullOrEmpty(libraryRoot))
{
return;
}

string docSource = Path.Combine(libraryRoot, "DOCUMENTATION.MD");
if (!File.Exists(docSource))
{
return;
}

string relativeDocPath = GetRelativePath(projectRoot, docSource).Replace("\\", "/");
string pointerPath = Path.Combine(projectRoot, "NEXUS_UNITY_DOCUMENTATION.md");
string pointerContent = "# Nexus Unity Documentation\n\n" +
"The canonical Nexus Unity documentation is maintained in the package root:\n\n" +
"- [DOCUMENTATION.MD](" + relativeDocPath + ")\n\n" +
"Keep all edits in the package copy so changes stay with version control.\n";

File.WriteAllText(pointerPath, pointerContent);
}

private static string GetRelativePath(string fromPath, string toPath)
{
Uri fromUri = new Uri(AppendDirectorySeparator(Path.GetFullPath(fromPath)));
Uri toUri = new Uri(Path.GetFullPath(toPath));
Uri relativeUri = fromUri.MakeRelativeUri(toUri);
return Uri.UnescapeDataString(relativeUri.ToString());
}

private static string AppendDirectorySeparator(string path)
{
if (path.EndsWith(Path.DirectorySeparatorChar.ToString()) ||
path.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
{
return path;
}
return path + Path.DirectorySeparatorChar;
}

private static string FindBridgeScript()
{
string[] guids = AssetDatabase.FindAssets("nexus_unity_bridge");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (path.EndsWith("nexus_unity_bridge.py"))
{
return Path.GetFullPath(path);
}
}

string manualSearch = Path.Combine(Application.dataPath, "nexus_unity_bridge.py");
if (File.Exists(manualSearch))
{
return manualSearch;
}

foreach (string path in Directory.GetFiles(Application.dataPath, "*.py", SearchOption.AllDirectories))
{
if (path.EndsWith("nexus_unity_bridge.py")) return Path.GetFullPath(path);
}
return null;
}
}
}
2 changes: 2 additions & 0 deletions Editor/MCPCliInstaller.Deploy.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading