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
10 changes: 10 additions & 0 deletions MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Clients.Configurators
Expand All @@ -16,6 +18,14 @@ public ClaudeCodeConfigurator() : base(new McpClient
})
{ }

public override bool SupportsSkills => true;

public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".claude", "skills", "unity-mcp-skill");
}

public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Claude CLI is installed (comes with Claude Code)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public ClaudeDesktopConfigurator() : base(new McpClient
})
{ }

public override bool SupportsSkills => true;

public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".claude", "skills", "unity-mcp-skill");
}

public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Claude Desktop",
Expand Down
8 changes: 8 additions & 0 deletions MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public CodexConfigurator() : base(new McpClient
})
{ }

public override bool SupportsSkills => true;

public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".codex", "skills", "unity-mcp-skill");
}

public override IList<string> GetInstallationSteps() => new List<string>
{
"Run 'codex config edit' in a terminal\nOR open the config file at the path above",
Expand Down
6 changes: 6 additions & 0 deletions MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,11 @@ public interface IMcpClientConfigurator

/// <summary>Returns ordered human-readable installation steps.</summary>
System.Collections.Generic.IList<string> GetInstallationSteps();

/// <summary>True if this client supports skill installation/sync.</summary>
bool SupportsSkills { get; }

/// <summary>Returns the absolute path where skills should be installed, or null if unsupported.</summary>
string GetSkillInstallPath();
}
}
2 changes: 2 additions & 0 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ protected McpClientConfiguratorBase(McpClient client)
public McpStatus Status => client.status;
public ConfiguredTransport ConfiguredTransport => client.configuredTransport;
public virtual bool SupportsAutoConfigure => true;
public virtual bool SupportsSkills => false;
public virtual string GetConfigureActionLabel() => "Configure";
public virtual string GetSkillInstallPath() => null;

public abstract string GetConfigPath();
public abstract McpStatus CheckStatus(bool attemptAutoRewrite = true);
Expand Down
269 changes: 269 additions & 0 deletions MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Setup
{
public class McpForUnitySkillInstaller : EditorWindow
{
private const string RepoUrlKey = "UnityMcpSkillSync.RepoUrl";
private const string BranchKey = "UnityMcpSkillSync.Branch";
private const string CliKey = "UnityMcpSkillSync.Cli";
private const string InstallDirKey = "UnityMcpSkillSync.InstallDir";
private const string CodexCli = "codex";
private const string ClaudeCli = "claude";
private static readonly string[] BranchOptions = { "beta", "main" };
private static readonly string[] CliOptions = { CodexCli, ClaudeCli };

private string _repoUrl;
private string _targetBranch;
private string _cliType;
private string _installDir;
private Vector2 _scroll;
private volatile bool _isRunning;
private readonly ConcurrentQueue<string> _pendingLogs = new();
private readonly StringBuilder _logBuilder = new(4096);

[MenuItem("Window/MCP For Unity/Install(Sync) MCP Skill")]
public static void OpenWindow()
{
GetWindow<McpForUnitySkillInstaller>("Unity MCP Skill Install(Sync)");
}

private void OnEnable()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
_repoUrl = EditorPrefs.GetString(RepoUrlKey, "https://github.com/CoplayDev/unity-mcp");
_targetBranch = EditorPrefs.GetString(BranchKey, "beta");
if (!BranchOptions.Contains(_targetBranch))
{
_targetBranch = "beta";
}
_cliType = EditorPrefs.GetString(CliKey, CodexCli);
if (!CliOptions.Contains(_cliType))
{
_cliType = CodexCli;
}
_installDir = EditorPrefs.GetString(InstallDirKey, GetDefaultInstallDir(userHome, _cliType));
EditorApplication.update += OnEditorUpdate;
}

private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
EditorPrefs.SetString(RepoUrlKey, _repoUrl);
EditorPrefs.SetString(BranchKey, _targetBranch);
EditorPrefs.SetString(CliKey, _cliType);
EditorPrefs.SetString(InstallDirKey, _installDir);
}

private void OnGUI()
{
FlushPendingLogs();
EditorGUILayout.HelpBox("Sync Unity MCP Skill to the latest on the selected branch and output the changed file list.", MessageType.Info);
EditorGUILayout.Space(4f);

EditorGUILayout.LabelField("Config", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(_isRunning))
{
_repoUrl = EditorGUILayout.TextField("Repo URL", _repoUrl);
var branchIndex = Array.IndexOf(BranchOptions, _targetBranch);
if (branchIndex < 0)
{
branchIndex = 0;
}

var selectedBranchIndex = EditorGUILayout.Popup("Branch", branchIndex, BranchOptions);
_targetBranch = BranchOptions[selectedBranchIndex];

var cliIndex = Array.IndexOf(CliOptions, _cliType);
if (cliIndex < 0)
{
cliIndex = 0;
}

var selectedCliIndex = EditorGUILayout.Popup("CLI", cliIndex, CliOptions);
if (selectedCliIndex != cliIndex)
{
var previousCli = _cliType;
_cliType = CliOptions[selectedCliIndex];
TryApplyCliDefaultInstallPath(previousCli, _cliType);
}

_installDir = EditorGUILayout.TextField("Install Dir", _installDir);
}

EditorGUILayout.Space(8f);
EditorGUILayout.BeginHorizontal();
using (new EditorGUI.DisabledScope(_isRunning))
{
if (GUILayout.Button($"Sync Latest ({_targetBranch})", GUILayout.Height(32f)))
{
AppendLineImmediate("Sync task queued...");
AppendLineImmediate("Will use GitHub API to read the remote directory tree and perform incremental sync (no repository clone).");
RunSyncLatest();
}
}

if (GUILayout.Button("Clear Log", GUILayout.Width(100f), GUILayout.Height(32f)))
{
_logBuilder.Clear();
while (_pendingLogs.TryDequeue(out _))
{
}
}
EditorGUILayout.EndHorizontal();

EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("Output", EditorStyles.boldLabel);
_scroll = EditorGUILayout.BeginScrollView(_scroll);
EditorGUILayout.TextArea(_logBuilder.ToString(), GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}

private void OnEditorUpdate()
{
var changed = FlushPendingLogs();
if (_isRunning || changed)
{
Repaint();
}
}

private void RunSyncLatest()
{
if (_isRunning)
{
return;
}

_isRunning = true;
SkillSyncService.SyncAsync(_repoUrl, _installDir, _targetBranch,
line => _pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] {SanitizeLogLine(line)}"),
result =>
{
_isRunning = false;
if (result.Success)
{
_pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] Sync complete: +{result.Added} ~{result.Updated} -{result.Deleted}");
}
else
{
_pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] [ERROR] {result.Error}");
}
});
}

private void TryApplyCliDefaultInstallPath(string previousCli, string currentCli)
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var previousDefaultInstall = GetDefaultInstallDir(userHome, previousCli);
var currentDefaultInstall = GetDefaultInstallDir(userHome, currentCli);

if (string.IsNullOrWhiteSpace(_installDir) || PathsEqual(_installDir, previousDefaultInstall))
{
_installDir = currentDefaultInstall;
}
}

private static string GetDefaultInstallDir(string userHome, string cliType)
{
var baseDir = IsClaudeCli(cliType) ? ".claude" : ".codex";
return Path.Combine(userHome, baseDir, "skills/unity-mcp-skill");
}

private static bool IsClaudeCli(string cliType)
{
return string.Equals(cliType, ClaudeCli, StringComparison.Ordinal);
}

private static bool PathsEqual(string left, string right)
{
if (string.IsNullOrWhiteSpace(left) || string.IsNullOrWhiteSpace(right))
{
return false;
}

try
{
return string.Equals(
SkillSyncService.ExpandPath(left),
SkillSyncService.ExpandPath(right),
StringComparison.Ordinal);
}
catch
{
return false;
}
}

private void AppendLineImmediate(string line)
{
var sanitized = SanitizeLogLine(line);
if (string.IsNullOrWhiteSpace(sanitized))
{
return;
}

_logBuilder.AppendLine($"[{DateTime.Now:HH:mm:ss}] {sanitized}");
_scroll.y = float.MaxValue;
Repaint();
}

private bool FlushPendingLogs()
{
var hasNewLine = false;
while (_pendingLogs.TryDequeue(out var line))
{
_logBuilder.AppendLine(line);
hasNewLine = true;
}

if (hasNewLine)
{
_scroll.y = float.MaxValue;
}

return hasNewLine;
}

private static string SanitizeLogLine(string line)
{
if (string.IsNullOrEmpty(line))
{
return string.Empty;
}

var sb = new StringBuilder(line.Length);
var inEscape = false;
foreach (var ch in line)
{
if (inEscape)
{
if (ch >= '@' && ch <= '~')
{
inEscape = false;
}
continue;
}

if (ch == '\u001b')
{
inEscape = true;
continue;
}

if (ch == '\t' || (ch >= ' ' && ch != 127))
{
sb.Append(ch);
}
}

return sb.ToString().Trim();
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs.meta

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

Loading