升级XR插件版本
This commit is contained in:
8
Packages/MCPForUnity/Editor/Clients/Configurators.meta
Normal file
8
Packages/MCPForUnity/Editor/Clients/Configurators.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59ff83375c2c74c8385c4a22549778dd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class AntigravityConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public AntigravityConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Antigravity",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
|
||||
HttpUrlProperty = "serverUrl",
|
||||
DefaultUnityFields = { { "disabled", false } },
|
||||
StripEnvWhenNotRequired = true
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Open Antigravity",
|
||||
"Click the more_horiz menu in the Agent pane > MCP Servers",
|
||||
"Select 'Install' for Unity MCP or use the Configure button above",
|
||||
"Restart Antigravity if necessary"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 331b33961513042e3945d0a1d06615b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class CherryStudioConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public const string ClientName = "Cherry Studio";
|
||||
|
||||
public CherryStudioConfigurator() : base(new McpClient
|
||||
{
|
||||
name = ClientName,
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Cherry Studio", "config"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Cherry Studio", "config"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Cherry Studio", "config"),
|
||||
SupportsHttpTransport = false
|
||||
})
|
||||
{ }
|
||||
|
||||
public override bool SupportsAutoConfigure => false;
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Open Cherry Studio",
|
||||
"Go to Settings (⚙️) → MCP Server",
|
||||
"Click 'Add Server' button",
|
||||
"For STDIO mode (recommended):",
|
||||
" - Name: unity-mcp",
|
||||
" - Type: STDIO",
|
||||
" - Command: uvx",
|
||||
" - Arguments: Copy from the Manual Configuration JSON below",
|
||||
"Click Save and restart Cherry Studio",
|
||||
"",
|
||||
"Note: Cherry Studio uses UI-based configuration.",
|
||||
"Use the manual snippet below as reference for the values to enter."
|
||||
};
|
||||
|
||||
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured, "Cherry Studio requires manual UI configuration");
|
||||
return client.status;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cherry Studio uses UI-based configuration. " +
|
||||
"Please use the Manual Configuration snippet and Installation Steps to configure manually."
|
||||
);
|
||||
}
|
||||
|
||||
public override string GetManualSnippet()
|
||||
{
|
||||
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
|
||||
if (useHttp)
|
||||
{
|
||||
return "# Cherry Studio does not support WebSocket transport.\n" +
|
||||
"# Cherry Studio supports STDIO and SSE transports.\n" +
|
||||
"# \n" +
|
||||
"# To use Cherry Studio:\n" +
|
||||
"# 1. Switch transport to 'Stdio' in Advanced Settings below\n" +
|
||||
"# 2. Return to this configuration screen\n" +
|
||||
"# 3. Copy the STDIO configuration snippet that will appear\n" +
|
||||
"# \n" +
|
||||
"# OPTION 2: SSE mode (future support)\n" +
|
||||
"# Note: Unity MCP does not currently have an SSE endpoint.\n" +
|
||||
"# This may be added in a future update.";
|
||||
}
|
||||
|
||||
return base.GetManualSnippet() + "\n\n" +
|
||||
"# Cherry Studio Configuration Instructions:\n" +
|
||||
"# Cherry Studio uses UI-based configuration, not a JSON file.\n" +
|
||||
"# \n" +
|
||||
"# To configure:\n" +
|
||||
"# 1. Open Cherry Studio\n" +
|
||||
"# 2. Go to Settings (⚙️) → MCP Server\n" +
|
||||
"# 3. Click 'Add Server'\n" +
|
||||
"# 4. Enter the following values from the JSON above:\n" +
|
||||
"# - Name: unity-mcp\n" +
|
||||
"# - Type: STDIO\n" +
|
||||
"# - Command: (copy 'command' value from JSON)\n" +
|
||||
"# - Arguments: (copy 'args' array values, space-separated or as individual entries)\n" +
|
||||
"# - Active: true\n" +
|
||||
"# 5. Click Save\n" +
|
||||
"# 6. Restart Cherry Studio";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6de06c6bb0399154d840a1e4c84be869
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
/// <summary>
|
||||
/// Claude Code configurator using the CLI-based registration (claude mcp add/remove).
|
||||
/// This integrates with Claude Code's native MCP management.
|
||||
/// </summary>
|
||||
public class ClaudeCodeConfigurator : ClaudeCliMcpConfigurator
|
||||
{
|
||||
public ClaudeCodeConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Claude Code",
|
||||
SupportsHttpTransport = true,
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Ensure Claude CLI is installed (comes with Claude Code)",
|
||||
"Click Register to add UnityMCP via 'claude mcp add'",
|
||||
"The server will be automatically available in Claude Code",
|
||||
"Use Unregister to remove via 'claude mcp remove'"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0d22681fc594475db1c189f2d9abdf7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class ClaudeDesktopConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public const string ClientName = "Claude Desktop";
|
||||
|
||||
public ClaudeDesktopConfigurator() : base(new McpClient
|
||||
{
|
||||
name = ClientName,
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Claude", "claude_desktop_config.json"),
|
||||
SupportsHttpTransport = false,
|
||||
StripEnvWhenNotRequired = true
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Open Claude Desktop",
|
||||
"Go to Settings > Developer > Edit Config\nOR open the config path",
|
||||
"Paste the configuration JSON",
|
||||
"Save and restart Claude Desktop"
|
||||
};
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
if (useHttp)
|
||||
{
|
||||
throw new InvalidOperationException("Claude Desktop does not support HTTP transport. Switch to stdio in settings before configuring.");
|
||||
}
|
||||
|
||||
base.Configure();
|
||||
}
|
||||
|
||||
public override string GetManualSnippet()
|
||||
{
|
||||
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
if (useHttp)
|
||||
{
|
||||
return "# Claude Desktop does not support HTTP transport.\n" +
|
||||
"# Open Advanced Settings and disable HTTP transport to use stdio, then regenerate.";
|
||||
}
|
||||
|
||||
return base.GetManualSnippet();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5e5d87c9db57495f842dc366f1ebd65
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the CodeBuddy CLI (~/.codebuddy.json) MCP settings.
|
||||
/// </summary>
|
||||
public class CodeBuddyCliConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public CodeBuddyCliConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "CodeBuddy CLI",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Install CodeBuddy CLI and ensure '~/.codebuddy.json' exists",
|
||||
"Click Configure to add the UnityMCP entry (or manually edit the file above)",
|
||||
"Restart your CLI session if needed"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 923728a98c8c74cfaa6e9203c408f34e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class CodexConfigurator : CodexMcpConfigurator
|
||||
{
|
||||
public CodexConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Codex",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml")
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Run 'codex config edit' in a terminal\nOR open the config file at the path above",
|
||||
"Paste the configuration TOML",
|
||||
"Save and restart Codex"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7037ef8b168e49f79247cb31c3be75a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class CopilotCliConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public CopilotCliConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "GitHub Copilot CLI",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json")
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Install GitHub Copilot CLI (https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)",
|
||||
"Open or create mcp-config.json at the path above",
|
||||
"Paste the configuration JSON (or use /mcp add in the CLI)",
|
||||
"Restart your Copilot CLI session"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14a4b9a7f749248d496466c2a3a53e56
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class CursorConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public CursorConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Cursor",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json")
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Open Cursor",
|
||||
"Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\nOR open the config file at the path above",
|
||||
"Paste the configuration JSON",
|
||||
"Save and restart Cursor"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b708eda314746481fb8f4a1fb0652b03
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class KiloCodeConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public KiloCodeConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Kilo Code",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
|
||||
IsVsCodeLayout = true
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Install Kilo Code extension in VS Code",
|
||||
"Open Kilo Code settings (gear icon in sidebar)",
|
||||
"Navigate to MCP Servers section and click 'Edit Global MCP Settings'\nOR open the config file at the path above",
|
||||
"Paste the configuration JSON into the mcpServers object",
|
||||
"Save and restart VS Code"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3286d62ffe5644f5ea60488fd7e6513d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class KiroConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public KiroConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Kiro",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
|
||||
EnsureEnvObject = true,
|
||||
DefaultUnityFields = { { "disabled", false } }
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Open Kiro",
|
||||
"Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\nOR open the config file at the path above",
|
||||
"Paste the configuration JSON",
|
||||
"Save and restart Kiro"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9b73ff071a6043dda1f2ec7d682ef71
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
/// <summary>
|
||||
/// Configurator for OpenCode (opencode.ai) - a Go-based terminal AI coding assistant.
|
||||
/// OpenCode uses ~/.config/opencode/opencode.json with a custom "mcp" format.
|
||||
/// </summary>
|
||||
public class OpenCodeConfigurator : McpClientConfiguratorBase
|
||||
{
|
||||
private const string ServerName = "unityMCP";
|
||||
private const string SchemaUrl = "https://opencode.ai/config.json";
|
||||
|
||||
public OpenCodeConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "OpenCode",
|
||||
windowsConfigPath = BuildConfigPath(),
|
||||
macConfigPath = BuildConfigPath(),
|
||||
linuxConfigPath = BuildConfigPath()
|
||||
})
|
||||
{ }
|
||||
|
||||
private static string BuildConfigPath()
|
||||
{
|
||||
string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
|
||||
string configBase = !string.IsNullOrEmpty(xdgConfigHome)
|
||||
? xdgConfigHome
|
||||
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
|
||||
return Path.Combine(configBase, "opencode", "opencode.json");
|
||||
}
|
||||
|
||||
public override string GetConfigPath() => CurrentOsPath();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load and parse the config file.
|
||||
/// Returns null if file doesn't exist or cannot be read.
|
||||
/// Returns parsed JObject if valid JSON found.
|
||||
/// Logs warning if file exists but contains malformed JSON.
|
||||
/// </summary>
|
||||
private JObject TryLoadConfig(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
string content;
|
||||
try
|
||||
{
|
||||
content = File.ReadAllText(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Failed to read config file {path}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<JObject>(content) ?? new JObject();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
// Malformed JSON - log warning and return null.
|
||||
// When Configure() receives null, it will do: TryLoadConfig(path) ?? new JObject()
|
||||
// This creates a fresh empty JObject, which replaces the entire file with only the unityMCP section.
|
||||
// Existing config sections are lost. To preserve sections, a different recovery strategy
|
||||
// (e.g., line-by-line parsing, JSON repair, or manual user intervention) would be needed.
|
||||
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Malformed JSON in {path}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = GetConfigPath();
|
||||
var config = TryLoadConfig(path);
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
return client.status;
|
||||
}
|
||||
|
||||
var unityMcp = config["mcp"]?[ServerName] as JObject;
|
||||
|
||||
if (unityMcp == null)
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
return client.status;
|
||||
}
|
||||
|
||||
string configuredUrl = unityMcp["url"]?.ToString();
|
||||
string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
|
||||
if (UrlsEqual(configuredUrl, expectedUrl))
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
}
|
||||
else if (attemptAutoRewrite)
|
||||
{
|
||||
Configure();
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client.SetStatus(McpStatus.Error, ex.Message);
|
||||
}
|
||||
|
||||
return client.status;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = GetConfigPath();
|
||||
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
|
||||
|
||||
// Load existing config or start fresh, preserving all other properties and MCP servers
|
||||
var config = TryLoadConfig(path) ?? new JObject();
|
||||
|
||||
// Only add $schema if creating a new file
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
config["$schema"] = SchemaUrl;
|
||||
}
|
||||
|
||||
// Preserve existing mcp section and only update our server entry
|
||||
var mcpSection = config["mcp"] as JObject ?? new JObject();
|
||||
config["mcp"] = mcpSection;
|
||||
|
||||
mcpSection[ServerName] = BuildServerEntry();
|
||||
|
||||
McpConfigurationHelper.WriteAtomicFile(path, JsonConvert.SerializeObject(config, Formatting.Indented));
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client.SetStatus(McpStatus.Error, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetManualSnippet()
|
||||
{
|
||||
var snippet = new JObject
|
||||
{
|
||||
["mcp"] = new JObject { [ServerName] = BuildServerEntry() }
|
||||
};
|
||||
return JsonConvert.SerializeObject(snippet, Formatting.Indented);
|
||||
}
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Install OpenCode (https://opencode.ai)",
|
||||
"Click Configure to add Unity MCP to ~/.config/opencode/opencode.json",
|
||||
"Restart OpenCode",
|
||||
"The Unity MCP server should be detected automatically"
|
||||
};
|
||||
|
||||
private static JObject BuildServerEntry() => new JObject
|
||||
{
|
||||
["type"] = "remote",
|
||||
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
|
||||
["enabled"] = true
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 489f99ffb7e6743e88e3203552c8b37b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class RiderConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public RiderConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Rider GitHub Copilot",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "github-copilot", "intellij", "mcp.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "github-copilot", "intellij", "mcp.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "github-copilot", "intellij", "mcp.json"),
|
||||
IsVsCodeLayout = true
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Install GitHub Copilot plugin in Rider",
|
||||
"Open or create mcp.json at the path above",
|
||||
"Paste the configuration JSON",
|
||||
"Save and restart Rider"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2511b0d05271d486bb61f8cc9fd11363
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class TraeConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public TraeConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Trae",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Trae", "mcp.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Trae", "mcp.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Trae", "mcp.json"),
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Open Trae and go to Settings > MCP",
|
||||
"Select Add Server > Add Manually",
|
||||
"Paste the JSON or point to the mcp.json file\n"+
|
||||
"Windows: %AppData%\\Trae\\mcp.json\n" +
|
||||
"macOS: ~/Library/Application Support/Trae/mcp.json\n" +
|
||||
"Linux: ~/.config/Trae/mcp.json\n",
|
||||
"Save and restart Trae"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3ab39e22ae0948ab94beae307f9902e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class VSCodeConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public VSCodeConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "VSCode GitHub Copilot",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "mcp.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "mcp.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "mcp.json"),
|
||||
IsVsCodeLayout = true
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Install GitHub Copilot extension",
|
||||
"Open or create mcp.json at the path above",
|
||||
"Paste the configuration JSON",
|
||||
"Save and restart VSCode"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcc7ead475a4d4ea2978151c217757b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class VSCodeInsidersConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public VSCodeInsidersConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "VSCode Insiders GitHub Copilot",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code - Insiders", "User", "mcp.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code - Insiders", "User", "mcp.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code - Insiders", "User", "mcp.json"),
|
||||
IsVsCodeLayout = true
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Install GitHub Copilot extension in VS Code Insiders",
|
||||
"Open or create mcp.json at the path above",
|
||||
"Paste the configuration JSON",
|
||||
"Save and restart VS Code Insiders"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c4a1b0d3b34489cbf0f8c40c49c4f3b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients.Configurators
|
||||
{
|
||||
public class WindsurfConfigurator : JsonFileMcpConfigurator
|
||||
{
|
||||
public WindsurfConfigurator() : base(new McpClient
|
||||
{
|
||||
name = "Windsurf",
|
||||
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
|
||||
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
|
||||
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
|
||||
HttpUrlProperty = "serverUrl",
|
||||
DefaultUnityFields = { { "disabled", false } },
|
||||
StripEnvWhenNotRequired = true
|
||||
})
|
||||
{ }
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Open Windsurf",
|
||||
"Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\nOR open the config file at the path above",
|
||||
"Paste the configuration JSON",
|
||||
"Save and restart Windsurf"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b528971e189f141d38db577f155bd222
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Contract for MCP client configurators. Each client is responsible for
|
||||
/// status detection, auto-configure, and manual snippet/steps.
|
||||
/// </summary>
|
||||
public interface IMcpClientConfigurator
|
||||
{
|
||||
/// <summary>Stable identifier (e.g., "cursor").</summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>Display name shown in the UI.</summary>
|
||||
string DisplayName { get; }
|
||||
|
||||
/// <summary>Current status cached by the configurator.</summary>
|
||||
McpStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The transport type the client is currently configured for.
|
||||
/// Returns Unknown if the client is not configured or the transport cannot be determined.
|
||||
/// </summary>
|
||||
ConfiguredTransport ConfiguredTransport { get; }
|
||||
|
||||
/// <summary>True if this client supports auto-configure.</summary>
|
||||
bool SupportsAutoConfigure { get; }
|
||||
|
||||
/// <summary>Label to show on the configure button for the current state.</summary>
|
||||
string GetConfigureActionLabel();
|
||||
|
||||
/// <summary>Returns the platform-specific config path (or message for CLI-managed clients).</summary>
|
||||
string GetConfigPath();
|
||||
|
||||
/// <summary>Checks and updates status; returns current status.</summary>
|
||||
McpStatus CheckStatus(bool attemptAutoRewrite = true);
|
||||
|
||||
/// <summary>Runs auto-configuration (register/write file/CLI etc.).</summary>
|
||||
void Configure();
|
||||
|
||||
/// <summary>Returns the manual configuration snippet (JSON/TOML/commands).</summary>
|
||||
string GetManualSnippet();
|
||||
|
||||
/// <summary>Returns ordered human-readable installation steps.</summary>
|
||||
System.Collections.Generic.IList<string> GetInstallationSteps();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5a5078d9e6e14027a1abfebf4018634
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
925
Packages/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Normal file
925
Packages/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Normal file
@@ -0,0 +1,925 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients
|
||||
{
|
||||
/// <summary>Shared base class for MCP configurators.</summary>
|
||||
public abstract class McpClientConfiguratorBase : IMcpClientConfigurator
|
||||
{
|
||||
protected readonly McpClient client;
|
||||
|
||||
protected McpClientConfiguratorBase(McpClient client)
|
||||
{
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
internal McpClient Client => client;
|
||||
|
||||
public string Id => client.name.Replace(" ", "").ToLowerInvariant();
|
||||
public virtual string DisplayName => client.name;
|
||||
public McpStatus Status => client.status;
|
||||
public ConfiguredTransport ConfiguredTransport => client.configuredTransport;
|
||||
public virtual bool SupportsAutoConfigure => true;
|
||||
public virtual string GetConfigureActionLabel() => "Configure";
|
||||
|
||||
public abstract string GetConfigPath();
|
||||
public abstract McpStatus CheckStatus(bool attemptAutoRewrite = true);
|
||||
public abstract void Configure();
|
||||
public abstract string GetManualSnippet();
|
||||
public abstract IList<string> GetInstallationSteps();
|
||||
|
||||
protected string GetUvxPathOrError()
|
||||
{
|
||||
string uvx = MCPServiceLocator.Paths.GetUvxPath();
|
||||
if (string.IsNullOrEmpty(uvx))
|
||||
{
|
||||
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
|
||||
}
|
||||
return uvx;
|
||||
}
|
||||
|
||||
protected string CurrentOsPath()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return client.windowsConfigPath;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
return client.macConfigPath;
|
||||
return client.linuxConfigPath;
|
||||
}
|
||||
|
||||
protected bool UrlsEqual(string a, string b)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(a.Trim(), UriKind.Absolute, out var uriA) &&
|
||||
Uri.TryCreate(b.Trim(), UriKind.Absolute, out var uriB))
|
||||
{
|
||||
return Uri.Compare(
|
||||
uriA,
|
||||
uriB,
|
||||
UriComponents.HttpRequestUrl,
|
||||
UriFormat.SafeUnescaped,
|
||||
StringComparison.OrdinalIgnoreCase) == 0;
|
||||
}
|
||||
|
||||
string Normalize(string value) => value.Trim().TrimEnd('/');
|
||||
return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>JSON-file based configurator (Cursor, Windsurf, VS Code, etc.).</summary>
|
||||
public abstract class JsonFileMcpConfigurator : McpClientConfiguratorBase
|
||||
{
|
||||
public JsonFileMcpConfigurator(McpClient client) : base(client) { }
|
||||
|
||||
public override string GetConfigPath() => CurrentOsPath();
|
||||
|
||||
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = GetConfigPath();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
return client.status;
|
||||
}
|
||||
|
||||
string configJson = File.ReadAllText(path);
|
||||
string[] args = null;
|
||||
string configuredUrl = null;
|
||||
bool configExists = false;
|
||||
|
||||
if (client.IsVsCodeLayout)
|
||||
{
|
||||
var vsConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
|
||||
if (vsConfig != null)
|
||||
{
|
||||
var unityToken =
|
||||
vsConfig["servers"]?["unityMCP"]
|
||||
?? vsConfig["mcp"]?["servers"]?["unityMCP"];
|
||||
|
||||
if (unityToken is JObject unityObj)
|
||||
{
|
||||
configExists = true;
|
||||
|
||||
var argsToken = unityObj["args"];
|
||||
if (argsToken is JArray)
|
||||
{
|
||||
args = argsToken.ToObject<string[]>();
|
||||
}
|
||||
|
||||
var urlToken = unityObj["url"] ?? unityObj["serverUrl"];
|
||||
if (urlToken != null && urlToken.Type != JTokenType.Null)
|
||||
{
|
||||
configuredUrl = urlToken.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
|
||||
if (standardConfig?.mcpServers?.unityMCP != null)
|
||||
{
|
||||
args = standardConfig.mcpServers.unityMCP.args;
|
||||
configuredUrl = standardConfig.mcpServers.unityMCP.url;
|
||||
configExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!configExists)
|
||||
{
|
||||
client.SetStatus(McpStatus.MissingConfig);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
return client.status;
|
||||
}
|
||||
|
||||
// Determine and set the configured transport type
|
||||
if (args != null && args.Length > 0)
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Stdio;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(configuredUrl))
|
||||
{
|
||||
// Distinguish HTTP Local from HTTP Remote by matching against both URLs
|
||||
string localRpcUrl = HttpEndpointUtility.GetLocalMcpRpcUrl();
|
||||
string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl();
|
||||
if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(configuredUrl, remoteRpcUrl))
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.HttpRemote;
|
||||
}
|
||||
else
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Http;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
bool matches = false;
|
||||
if (args != null && args.Length > 0)
|
||||
{
|
||||
string expectedUvxUrl = AssetPathUtility.GetMcpServerPackageSource();
|
||||
string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args);
|
||||
matches = !string.IsNullOrEmpty(configuredUvxUrl) &&
|
||||
McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(configuredUrl))
|
||||
{
|
||||
// Match against the active scope's URL
|
||||
string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
matches = UrlsEqual(configuredUrl, expectedUrl);
|
||||
}
|
||||
|
||||
if (matches)
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
return client.status;
|
||||
}
|
||||
|
||||
if (attemptAutoRewrite)
|
||||
{
|
||||
var result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
|
||||
if (result == "Configured successfully")
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client.SetStatus(McpStatus.Error, ex.Message);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
return client.status;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
string path = GetConfigPath();
|
||||
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
|
||||
string result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
|
||||
if (result == "Configured successfully")
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(result);
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetManualSnippet()
|
||||
{
|
||||
try
|
||||
{
|
||||
string uvx = GetUvxPathOrError();
|
||||
return ConfigJsonBuilder.BuildManualConfigJson(uvx, client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorObj = new { error = ex.Message };
|
||||
return JsonConvert.SerializeObject(errorObj);
|
||||
}
|
||||
}
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string> { "Configuration steps not available for this client." };
|
||||
}
|
||||
|
||||
/// <summary>Codex (TOML) configurator.</summary>
|
||||
public abstract class CodexMcpConfigurator : McpClientConfiguratorBase
|
||||
{
|
||||
public CodexMcpConfigurator(McpClient client) : base(client) { }
|
||||
|
||||
public override string GetConfigPath() => CurrentOsPath();
|
||||
|
||||
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = GetConfigPath();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
return client.status;
|
||||
}
|
||||
|
||||
string toml = File.ReadAllText(path);
|
||||
if (CodexConfigHelper.TryParseCodexServer(toml, out _, out var args, out var url))
|
||||
{
|
||||
// Determine and set the configured transport type
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
// Distinguish HTTP Local from HTTP Remote
|
||||
string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl();
|
||||
if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(url, remoteRpcUrl))
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.HttpRemote;
|
||||
}
|
||||
else
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Http;
|
||||
}
|
||||
}
|
||||
else if (args != null && args.Length > 0)
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Stdio;
|
||||
}
|
||||
else
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
bool matches = false;
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
// Match against the active scope's URL
|
||||
matches = UrlsEqual(url, HttpEndpointUtility.GetMcpRpcUrl());
|
||||
}
|
||||
else if (args != null && args.Length > 0)
|
||||
{
|
||||
string expected = AssetPathUtility.GetMcpServerPackageSource();
|
||||
string configured = McpConfigurationHelper.ExtractUvxUrl(args);
|
||||
matches = !string.IsNullOrEmpty(configured) &&
|
||||
McpConfigurationHelper.PathsEqual(configured, expected);
|
||||
}
|
||||
|
||||
if (matches)
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
if (attemptAutoRewrite)
|
||||
{
|
||||
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
|
||||
if (result == "Configured successfully")
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client.SetStatus(McpStatus.Error, ex.Message);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
return client.status;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
string path = GetConfigPath();
|
||||
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
|
||||
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
|
||||
if (result == "Configured successfully")
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(result);
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetManualSnippet()
|
||||
{
|
||||
try
|
||||
{
|
||||
string uvx = GetUvxPathOrError();
|
||||
return CodexConfigHelper.BuildCodexServerBlock(uvx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"# error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Run 'codex config edit' or open the config path",
|
||||
"Paste the TOML",
|
||||
"Save and restart Codex"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>CLI-based configurator (Claude Code).</summary>
|
||||
public abstract class ClaudeCliMcpConfigurator : McpClientConfiguratorBase
|
||||
{
|
||||
public ClaudeCliMcpConfigurator(McpClient client) : base(client) { }
|
||||
|
||||
public override bool SupportsAutoConfigure => true;
|
||||
public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Register";
|
||||
|
||||
public override string GetConfigPath() => "Managed via Claude CLI";
|
||||
|
||||
/// <summary>
|
||||
/// Checks the Claude CLI registration status.
|
||||
/// MUST be called from the main Unity thread due to EditorPrefs and Application.dataPath access.
|
||||
/// </summary>
|
||||
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
|
||||
{
|
||||
// Capture main-thread-only values before delegating to thread-safe method
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
// Resolve claudePath on the main thread (EditorPrefs access)
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, attemptAutoRewrite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal thread-safe version of CheckStatus.
|
||||
/// Can be called from background threads because all main-thread-only values are passed as parameters.
|
||||
/// projectDir, useHttpTransport, and claudePath are REQUIRED (non-nullable) to enforce thread safety at compile time.
|
||||
/// NOTE: attemptAutoRewrite is NOT fully thread-safe because Configure() requires the main thread.
|
||||
/// When called from a background thread, pass attemptAutoRewrite=false and handle re-registration
|
||||
/// on the main thread based on the returned status.
|
||||
/// </summary>
|
||||
internal McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, string claudePath, bool attemptAutoRewrite = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured, "Claude CLI not found");
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
return client.status;
|
||||
}
|
||||
|
||||
// projectDir is required - no fallback to Application.dataPath
|
||||
if (string.IsNullOrEmpty(projectDir))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided for thread-safe execution");
|
||||
}
|
||||
|
||||
string pathPrepend = null;
|
||||
if (Application.platform == RuntimePlatform.OSXEditor)
|
||||
{
|
||||
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||
{
|
||||
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string claudeDir = Path.GetDirectoryName(claudePath);
|
||||
if (!string.IsNullOrEmpty(claudeDir))
|
||||
{
|
||||
pathPrepend = string.IsNullOrEmpty(pathPrepend)
|
||||
? claudeDir
|
||||
: $"{claudeDir}:{pathPrepend}";
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Check if UnityMCP exists (handles both "UnityMCP" and legacy "unityMCP")
|
||||
if (ExecPath.TryRun(claudePath, "mcp list", projectDir, out var listStdout, out var listStderr, 10000, pathPrepend))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(listStdout) && listStdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
// UnityMCP is registered - now verify transport mode matches
|
||||
// useHttpTransport parameter is required (non-nullable) to ensure thread safety
|
||||
bool currentUseHttp = useHttpTransport;
|
||||
|
||||
// Get detailed info about the registration to check transport type
|
||||
// Try both "UnityMCP" and "unityMCP" (legacy naming)
|
||||
string getStdout = null, getStderr = null;
|
||||
bool gotInfo = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out getStdout, out getStderr, 7000, pathPrepend)
|
||||
|| ExecPath.TryRun(claudePath, "mcp get unityMCP", projectDir, out getStdout, out getStderr, 7000, pathPrepend);
|
||||
if (gotInfo)
|
||||
{
|
||||
// Parse the output to determine registered transport mode
|
||||
// The CLI output format contains "Type: http" or "Type: stdio"
|
||||
bool registeredWithHttp = getStdout.Contains("Type: http", StringComparison.OrdinalIgnoreCase);
|
||||
bool registeredWithStdio = getStdout.Contains("Type: stdio", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Set the configured transport based on what we detected
|
||||
// For HTTP, we can't distinguish local/remote from CLI output alone,
|
||||
// so infer from the current scope setting when HTTP is detected.
|
||||
if (registeredWithHttp)
|
||||
{
|
||||
client.configuredTransport = HttpEndpointUtility.IsRemoteScope()
|
||||
? Models.ConfiguredTransport.HttpRemote
|
||||
: Models.ConfiguredTransport.Http;
|
||||
}
|
||||
else if (registeredWithStdio)
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Stdio;
|
||||
}
|
||||
else
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
// Check for transport mismatch (3-way: Stdio, Http, HttpRemote)
|
||||
bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp);
|
||||
|
||||
// For stdio transport, also check package version
|
||||
bool hasVersionMismatch = false;
|
||||
string configuredPackageSource = null;
|
||||
string expectedPackageSource = null;
|
||||
if (registeredWithStdio)
|
||||
{
|
||||
expectedPackageSource = AssetPathUtility.GetMcpServerPackageSource();
|
||||
configuredPackageSource = ExtractPackageSourceFromCliOutput(getStdout);
|
||||
hasVersionMismatch = !string.IsNullOrEmpty(configuredPackageSource) &&
|
||||
!string.Equals(configuredPackageSource, expectedPackageSource, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// If there's any mismatch and auto-rewrite is enabled, re-register
|
||||
if (hasTransportMismatch || hasVersionMismatch)
|
||||
{
|
||||
// Configure() requires main thread (accesses EditorPrefs, Application.dataPath)
|
||||
// Only attempt auto-rewrite if we're on the main thread
|
||||
bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1;
|
||||
if (attemptAutoRewrite && isMainThread)
|
||||
{
|
||||
string reason = hasTransportMismatch
|
||||
? $"Transport mismatch (registered: {(registeredWithHttp ? "HTTP" : "stdio")}, expected: {(currentUseHttp ? "HTTP" : "stdio")})"
|
||||
: $"Package version mismatch (registered: {configuredPackageSource}, expected: {expectedPackageSource})";
|
||||
McpLog.Info($"{reason}. Re-registering...");
|
||||
try
|
||||
{
|
||||
// Force re-register by ensuring status is not Configured (which would toggle to Unregister)
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
Configure();
|
||||
return client.status;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Auto-reregister failed: {ex.Message}");
|
||||
client.SetStatus(McpStatus.IncorrectPath, $"Configuration mismatch. Click Configure to re-register.");
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasTransportMismatch)
|
||||
{
|
||||
string errorMsg = $"Transport mismatch: Claude Code is registered with {(registeredWithHttp ? "HTTP" : "stdio")} but current setting is {(currentUseHttp ? "HTTP" : "stdio")}. Click Configure to re-register.";
|
||||
client.SetStatus(McpStatus.Error, errorMsg);
|
||||
McpLog.Warn(errorMsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath, $"Package version mismatch: registered with '{configuredPackageSource}' but current version is '{expectedPackageSource}'.");
|
||||
}
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client.SetStatus(McpStatus.Error, ex.Message);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
return client.status;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
if (client.status == McpStatus.Configured)
|
||||
{
|
||||
Unregister();
|
||||
}
|
||||
else
|
||||
{
|
||||
Register();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe version of Configure that uses pre-captured main-thread values.
|
||||
/// All parameters must be captured on the main thread before calling this method.
|
||||
/// </summary>
|
||||
public void ConfigureWithCapturedValues(
|
||||
string projectDir, string claudePath, string pathPrepend,
|
||||
bool useHttpTransport, string httpUrl,
|
||||
string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh,
|
||||
string apiKey,
|
||||
Models.ConfiguredTransport serverTransport)
|
||||
{
|
||||
if (client.status == McpStatus.Configured)
|
||||
{
|
||||
UnregisterWithCapturedValues(projectDir, claudePath, pathPrepend);
|
||||
}
|
||||
else
|
||||
{
|
||||
RegisterWithCapturedValues(projectDir, claudePath, pathPrepend,
|
||||
useHttpTransport, httpUrl, uvxPath, gitUrl, packageName, shouldForceRefresh,
|
||||
apiKey, serverTransport);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe registration using pre-captured values.
|
||||
/// </summary>
|
||||
private void RegisterWithCapturedValues(
|
||||
string projectDir, string claudePath, string pathPrepend,
|
||||
bool useHttpTransport, string httpUrl,
|
||||
string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh,
|
||||
string apiKey,
|
||||
Models.ConfiguredTransport serverTransport)
|
||||
{
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
|
||||
}
|
||||
|
||||
string args;
|
||||
if (useHttpTransport)
|
||||
{
|
||||
// Only include API key header for remote-hosted mode
|
||||
if (serverTransport == Models.ConfiguredTransport.HttpRemote && !string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
string safeKey = SanitizeShellHeaderValue(apiKey);
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
|
||||
string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty;
|
||||
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}";
|
||||
}
|
||||
|
||||
// Remove any existing registrations - handle both "UnityMCP" and "unityMCP" (legacy)
|
||||
McpLog.Info("Removing any existing UnityMCP registrations before adding...");
|
||||
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
|
||||
// Now add the registration
|
||||
if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
|
||||
}
|
||||
|
||||
McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport.");
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
client.configuredTransport = serverTransport;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe unregistration using pre-captured values.
|
||||
/// </summary>
|
||||
private void UnregisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend)
|
||||
{
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
|
||||
}
|
||||
|
||||
// Remove both "UnityMCP" and "unityMCP" (legacy naming)
|
||||
McpLog.Info("Removing all UnityMCP registrations...");
|
||||
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
|
||||
McpLog.Info("MCP server successfully unregistered from Claude Code.");
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
private void Register()
|
||||
{
|
||||
var pathService = MCPServiceLocator.Paths;
|
||||
string claudePath = pathService.GetClaudeCliPath();
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
|
||||
}
|
||||
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
|
||||
string args;
|
||||
if (useHttpTransport)
|
||||
{
|
||||
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
// Only include API key header for remote-hosted mode
|
||||
if (HttpEndpointUtility.IsRemoteScope())
|
||||
{
|
||||
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
|
||||
if (!string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
string safeKey = SanitizeShellHeaderValue(apiKey);
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
|
||||
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
|
||||
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
|
||||
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
|
||||
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}";
|
||||
}
|
||||
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
|
||||
string pathPrepend = null;
|
||||
if (Application.platform == RuntimePlatform.OSXEditor)
|
||||
{
|
||||
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||
{
|
||||
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string claudeDir = Path.GetDirectoryName(claudePath);
|
||||
if (!string.IsNullOrEmpty(claudeDir))
|
||||
{
|
||||
pathPrepend = string.IsNullOrEmpty(pathPrepend)
|
||||
? claudeDir
|
||||
: $"{claudeDir}:{pathPrepend}";
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Remove any existing registrations - handle both "UnityMCP" and "unityMCP" (legacy)
|
||||
McpLog.Info("Removing any existing UnityMCP registrations before adding...");
|
||||
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
|
||||
// Now add the registration with the current transport mode
|
||||
if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
|
||||
}
|
||||
|
||||
McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport.");
|
||||
|
||||
// Set status to Configured immediately after successful registration
|
||||
// The UI will trigger an async verification check separately to avoid blocking
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
}
|
||||
|
||||
private void Unregister()
|
||||
{
|
||||
var pathService = MCPServiceLocator.Paths;
|
||||
string claudePath = pathService.GetClaudeCliPath();
|
||||
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
|
||||
}
|
||||
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
string pathPrepend = null;
|
||||
if (Application.platform == RuntimePlatform.OSXEditor)
|
||||
{
|
||||
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||
{
|
||||
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
|
||||
// Remove both "UnityMCP" and "unityMCP" (legacy naming)
|
||||
McpLog.Info("Removing all UnityMCP registrations...");
|
||||
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
|
||||
McpLog.Info("MCP server successfully unregistered from Claude Code.");
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
public override string GetManualSnippet()
|
||||
{
|
||||
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
|
||||
if (useHttpTransport)
|
||||
{
|
||||
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
// Only include API key header for remote-hosted mode
|
||||
string headerArg = "";
|
||||
if (HttpEndpointUtility.IsRemoteScope())
|
||||
{
|
||||
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
|
||||
headerArg = !string.IsNullOrEmpty(apiKey) ? $" --header \"{AuthConstants.ApiKeyHeader}: {SanitizeShellHeaderValue(apiKey)}\"" : "";
|
||||
}
|
||||
return "# Register the MCP server with Claude Code:\n" +
|
||||
$"claude mcp add --transport http UnityMCP {httpUrl}{headerArg}\n\n" +
|
||||
"# Unregister the MCP server:\n" +
|
||||
"claude mcp remove UnityMCP\n\n" +
|
||||
"# List registered servers:\n" +
|
||||
"claude mcp list";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(uvxPath))
|
||||
{
|
||||
return "# Error: Configuration not available - check paths in Advanced Settings";
|
||||
}
|
||||
|
||||
string packageSource = AssetPathUtility.GetMcpServerPackageSource();
|
||||
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
|
||||
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
|
||||
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
|
||||
|
||||
return "# Register the MCP server with Claude Code:\n" +
|
||||
$"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{packageSource}\" mcp-for-unity\n\n" +
|
||||
"# Unregister the MCP server:\n" +
|
||||
"claude mcp remove UnityMCP\n\n" +
|
||||
"# List registered servers:\n" +
|
||||
"claude mcp list";
|
||||
}
|
||||
|
||||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Ensure Claude CLI is installed",
|
||||
"Use Register to add UnityMCP (or run claude mcp add UnityMCP)",
|
||||
"Restart Claude Code"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a value for safe inclusion inside a double-quoted shell argument.
|
||||
/// Escapes characters that are special within double quotes (", \, `, $, !)
|
||||
/// to prevent shell injection or argument splitting.
|
||||
/// </summary>
|
||||
private static string SanitizeShellHeaderValue(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return value;
|
||||
|
||||
var sb = new System.Text.StringBuilder(value.Length);
|
||||
foreach (char c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
case '\\':
|
||||
case '`':
|
||||
case '$':
|
||||
case '!':
|
||||
sb.Append('\\');
|
||||
sb.Append(c);
|
||||
break;
|
||||
default:
|
||||
sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the package source (--from argument value) from claude mcp get output.
|
||||
/// The output format includes args like: --from "mcpforunityserver==9.0.1"
|
||||
/// </summary>
|
||||
private static string ExtractPackageSourceFromCliOutput(string cliOutput)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cliOutput))
|
||||
return null;
|
||||
|
||||
// Look for --from followed by the package source
|
||||
// The CLI output may have it quoted or unquoted
|
||||
int fromIndex = cliOutput.IndexOf("--from", StringComparison.OrdinalIgnoreCase);
|
||||
if (fromIndex < 0)
|
||||
return null;
|
||||
|
||||
// Move past "--from" and any whitespace
|
||||
int startIndex = fromIndex + 6;
|
||||
while (startIndex < cliOutput.Length && char.IsWhiteSpace(cliOutput[startIndex]))
|
||||
startIndex++;
|
||||
|
||||
if (startIndex >= cliOutput.Length)
|
||||
return null;
|
||||
|
||||
// Check if value is quoted
|
||||
char quoteChar = cliOutput[startIndex];
|
||||
if (quoteChar == '"' || quoteChar == '\'')
|
||||
{
|
||||
startIndex++;
|
||||
int endIndex = cliOutput.IndexOf(quoteChar, startIndex);
|
||||
if (endIndex > startIndex)
|
||||
return cliOutput.Substring(startIndex, endIndex - startIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unquoted - read until whitespace or end of line
|
||||
int endIndex = startIndex;
|
||||
while (endIndex < cliOutput.Length && !char.IsWhiteSpace(cliOutput[endIndex]))
|
||||
endIndex++;
|
||||
|
||||
if (endIndex > startIndex)
|
||||
return cliOutput.Substring(startIndex, endIndex - startIndex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d408fd7733cb4a1eb80f785307db2ff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
60
Packages/MCPForUnity/Editor/Clients/McpClientRegistry.cs
Normal file
60
Packages/MCPForUnity/Editor/Clients/McpClientRegistry.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Central registry that auto-discovers configurators via TypeCache.
|
||||
/// </summary>
|
||||
public static class McpClientRegistry
|
||||
{
|
||||
private static List<IMcpClientConfigurator> cached;
|
||||
|
||||
public static IReadOnlyList<IMcpClientConfigurator> All
|
||||
{
|
||||
get
|
||||
{
|
||||
if (cached == null)
|
||||
{
|
||||
cached = BuildRegistry();
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<IMcpClientConfigurator> BuildRegistry()
|
||||
{
|
||||
var configurators = new List<IMcpClientConfigurator>();
|
||||
|
||||
foreach (var type in TypeCache.GetTypesDerivedFrom<IMcpClientConfigurator>())
|
||||
{
|
||||
if (type.IsAbstract || !type.IsClass || !type.IsPublic)
|
||||
continue;
|
||||
|
||||
// Require a public parameterless constructor
|
||||
if (type.GetConstructor(Type.EmptyTypes) == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
if (Activator.CreateInstance(type) is IMcpClientConfigurator instance)
|
||||
{
|
||||
configurators.Add(instance);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"UnityMCP: Failed to instantiate configurator {type.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Alphabetical order by display name
|
||||
configurators = configurators.OrderBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
return configurators;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ce08555f995e4e848a826c63f18cb35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user