Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ac93422
feat: implement optional IP/token authentication for HTTP/WebSocket c…
Jordonh18 Dec 5, 2025
bfa9cff
feat: add optional authentication settings and UI integration
Jordonh18 Dec 5, 2025
b65afcc
feat: integrate authentication handling in WebSocket and client confi…
Jordonh18 Dec 5, 2025
f738b59
feat: add AuthPreferencesUtility meta file for authentication integra…
Jordonh18 Dec 5, 2025
a4406d4
feat: implement authentication section with token management and UI i…
Jordonh18 Dec 5, 2025
c1bb60f
feat: add Auth.meta file for authentication integration
Jordonh18 Dec 5, 2025
b74cbeb
feat: update authentication handling and command generation for serve…
Jordonh18 Dec 5, 2025
2058843
feat: remove authorization input handling from manual config JSON bui…
Jordonh18 Dec 5, 2025
7ea878d
feat: enhance authentication handling with environment variable suppo…
Jordonh18 Dec 5, 2025
ac6b978
feat: enhance config JSON builder to support authorization header and…
Jordonh18 Dec 5, 2025
7517d4a
feat: enhance ConfigJsonBuilder to support Authorization header input…
Jordonh18 Dec 5, 2025
00357a2
feat: streamline authentication handling by passing flags directly to…
Jordonh18 Dec 5, 2025
2be373b
feat: enforce authentication by always requiring auth token and allow…
Jordonh18 Dec 5, 2025
299ddbb
Refactor authentication system to use API key
Jordonh18 Dec 5, 2025
c72cdb9
feat: refactor API key management to ensure existence and update UI f…
Jordonh18 Dec 5, 2025
a0656fa
feat: ensure plugin hub and services utilize final authentication set…
Jordonh18 Dec 5, 2025
5ad84d2
feat: enhance API key management and logging in authentication process
Jordonh18 Dec 5, 2025
59fd5a8
feat: add API key path logging to authentication setup
Jordonh18 Dec 5, 2025
ada4559
feat: ensure authentication is always enabled and token is set in Aut…
Jordonh18 Dec 5, 2025
b79601a
feat: update API key generation to use base64url encoding and enhance…
Jordonh18 Dec 5, 2025
7c9db64
feat: enhance logging setup for improved startup visibility and authe…
Jordonh18 Dec 5, 2025
88a2c2c
feat: add API key to WebSocket handshake and update server package ve…
Jordonh18 Dec 5, 2025
24d0238
feat: add error handling for WebSocket connection failures with speci…
Jordonh18 Dec 5, 2025
a39d86d
feat: enhance WebSocket authentication handling by logging missing re…
Jordonh18 Dec 5, 2025
ff37584
feat: enhance API key validation logging and add HTTP auth guard midd…
Jordonh18 Dec 5, 2025
8310b26
feat: enhance logging for HTTP auth guard middleware registration and…
Jordonh18 Dec 5, 2025
ed0312a
feat: implement deferred registration for HTTP auth guard middleware …
Jordonh18 Dec 5, 2025
65036d2
feat: update HTTP server command hint for manual server start instruc…
Jordonh18 Dec 5, 2025
75b3bf8
feat: enhance unauthorized response with WWW-Authenticate header for …
Jordonh18 Dec 5, 2025
5f19710
feat: improve API key validation responses and add OAuth discovery en…
Jordonh18 Dec 5, 2025
74c144c
feat: change logging level to debug for authentication context and re…
Jordonh18 Dec 5, 2025
1d76a8f
feat: update README and server configuration to include X-API-Key hea…
Jordonh18 Dec 5, 2025
65a26d2
feat: integrate authentication settings with UI and server configuration
Jordonh18 Dec 8, 2025
ad22b28
feat: refactor authentication system with new settings and middleware…
Jordonh18 Dec 8, 2025
2fcf35d
feat: remove unnecessary comments for clarity in authentication module
Jordonh18 Dec 8, 2025
5fd5211
feat: update authentication to use X-API-Key instead of Bearer token …
Jordonh18 Dec 8, 2025
c21d146
fix: correct toggle element syntax in connection settings UI
Jordonh18 Dec 8, 2025
7807a04
feat: add version field to configuration models and welcome message
Jordonh18 Dec 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ internal static class EditorPrefKeys
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";

internal const string AuthToken = "MCPForUnity.AuthToken";
internal const string AuthEnabled = "MCPForUnity.AuthEnabled";
internal const string AuthAllowedIps = "MCPForUnity.AuthAllowedIps";

internal const string ServerSrc = "MCPForUnity.ServerSrc";
internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer";
internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig";
Expand Down
1 change: 1 addition & 0 deletions MCPForUnity/Editor/Helpers/AssetPathUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ public static (string uvxPath, string fromUrl, string packageName) GetUvxCommand
{
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
string fromUrl = GetMcpServerGitUrl();
// Default uvx package name
string packageName = "mcp-for-unity";

return (uvxPath, fromUrl, packageName);
Expand Down
139 changes: 139 additions & 0 deletions MCPForUnity/Editor/Helpers/AuthPreferencesUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System;
using System.IO;
using MCPForUnity.Editor.Constants;
using UnityEditor;

namespace MCPForUnity.Editor.Helpers
{
internal static class AuthPreferencesUtility
{
private static string ApiKeyPrefKey => EditorPrefKeys.AuthToken;
private static string AuthEnabledPrefKey => EditorPrefKeys.AuthEnabled;
private static string AllowedIpsPrefKey => EditorPrefKeys.AuthAllowedIps;

internal static bool IsAuthEnabled()
{
return EditorPrefs.GetBool(AuthEnabledPrefKey, false);
}

internal static void SetAuthEnabled(bool enabled)
{
EditorPrefs.SetBool(AuthEnabledPrefKey, enabled);
}

internal static string GetAllowedIps()
{
string stored = EditorPrefs.GetString(AllowedIpsPrefKey, string.Empty);
return string.IsNullOrWhiteSpace(stored) ? "*" : stored;
}

internal static void SetAllowedIps(string allowedIps)
{
string value = string.IsNullOrWhiteSpace(allowedIps) ? "*" : allowedIps;
EditorPrefs.SetString(AllowedIpsPrefKey, value);
}

internal static string GetApiKey(bool ensureExists = true)
{
// Prefer EditorPrefs for quick access
string apiKey = EditorPrefs.GetString(ApiKeyPrefKey, string.Empty);

if (string.IsNullOrEmpty(apiKey) && ensureExists)
{
apiKey = TryReadApiKeyFromDisk();
if (string.IsNullOrEmpty(apiKey))
{
apiKey = GenerateNewApiKey();
}

EditorPrefs.SetString(ApiKeyPrefKey, apiKey);
TryPersistApiKey(apiKey);
}

return apiKey;
}

internal static void SetApiKey(string apiKey)
{
if (string.IsNullOrEmpty(apiKey))
{
apiKey = GenerateNewApiKey();
}

EditorPrefs.SetString(ApiKeyPrefKey, apiKey);
TryPersistApiKey(apiKey);
}

internal static string GenerateNewApiKey()
{
// 32 bytes -> ~43 base64url chars without padding; mirrors server token_urlsafe
var bytes = new byte[32];
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
{
rng.GetBytes(bytes);
}

string base64 = Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');

return base64;
}

internal static string GetApiKeyFilePath()
{
// Keep UI in lockstep with server path resolution (supports UNITY_MCP_HOME override)
string overrideRoot = Environment.GetEnvironmentVariable("UNITY_MCP_HOME");
if (!string.IsNullOrEmpty(overrideRoot))
{
return Path.Combine(overrideRoot, "api_key");
}

#if UNITY_EDITOR_WIN
string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(root, "UnityMCP", "api_key");
#elif UNITY_EDITOR_OSX
string root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", "UnityMCP");
return Path.Combine(root, "api_key");
#else
string root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".local", "share", "UnityMCP");
return Path.Combine(root, "api_key");
#endif
}

private static string TryReadApiKeyFromDisk()
{
try
{
string path = GetApiKeyFilePath();
if (!File.Exists(path))
{
return string.Empty;
}

string content = File.ReadAllText(path).Trim();
return content;
}
catch (Exception)
{
// Fall back to generating a new key if reading fails
return string.Empty;
}
}

private static void TryPersistApiKey(string apiKey)
{
try
{
string path = GetApiKeyFilePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllText(path, apiKey);
}
catch (Exception)
{
// Non-fatal: user can still copy the key from the UI
}
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Helpers/AuthPreferencesUtility.cs.meta

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

80 changes: 77 additions & 3 deletions MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ namespace MCPForUnity.Editor.Helpers
{
public static class ConfigJsonBuilder
{
private const string ApiKeyInputKey = "UnityMcpApiKey";
public static string BuildManualConfigJson(string uvPath, McpClient client)
{
var root = new JObject();
bool isVSCode = client?.IsVsCodeLayout == true;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");

var unity = new JObject();
PopulateUnityNode(unity, uvPath, client, isVSCode);
PopulateUnityNode(root, unity, uvPath, client, isVSCode);

container["unityMCP"] = unity;

Expand All @@ -35,7 +36,7 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa
bool isVSCode = client?.IsVsCodeLayout == true;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
JObject unity = container["unityMCP"] as JObject ?? new JObject();
PopulateUnityNode(unity, uvPath, client, isVSCode);
PopulateUnityNode(root, unity, uvPath, client, isVSCode);

container["unityMCP"] = unity;
return root;
Expand All @@ -48,10 +49,11 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa
/// - Adds transport configuration (HTTP or stdio)
/// - Adds disabled:false for Windsurf/Kiro only when missing
/// </summary>
private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode)
private static void PopulateUnityNode(JObject root, JObject unity, string uvPath, McpClient client, bool isVSCode)
{
// Get transport preference (default to HTTP)
bool useHttpTransport = client?.SupportsHttpTransport != false && EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
bool authEnabled = AuthPreferencesUtility.IsAuthEnabled();
string httpProperty = string.IsNullOrEmpty(client?.HttpUrlProperty) ? "url" : client.HttpUrlProperty;
var urlPropsToRemove = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" };
urlPropsToRemove.Remove(httpProperty);
Expand All @@ -62,6 +64,72 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
unity[httpProperty] = httpUrl;

var inputs = root["inputs"] as JArray ?? new JArray();

if (authEnabled)
{
// Add API key header with input binding and prompt definition
var headers = unity["headers"] as JObject ?? new JObject();
// Remove legacy auth header if present
if (headers["Authorization"] != null)
{
headers.Remove("Authorization");
}
headers["X-API-Key"] = "${input:UnityMcpApiKey}";
unity["headers"] = headers;

var existing = inputs
.OfType<JObject>()
.FirstOrDefault(o => string.Equals((string)o["id"], ApiKeyInputKey, StringComparison.Ordinal));
if (existing == null)
{
existing = new JObject();
inputs.Add(existing);
}

// Drop legacy Authorization input if it exists
foreach (var legacy in inputs
.OfType<JObject>()
.Where(o => string.Equals((string)o["id"], "Authorization", StringComparison.Ordinal))
.ToList())
{
inputs.Remove(legacy);
}

existing["id"] = ApiKeyInputKey;
existing["type"] = "promptString";
existing["description"] = "Unity MCP API Key";
existing["password"] = true;

root["inputs"] = inputs;
}
else
{
// Remove auth inputs and headers when disabled
foreach (var legacy in inputs
.OfType<JObject>()
.Where(o => string.Equals((string)o["id"], ApiKeyInputKey, StringComparison.Ordinal) ||
string.Equals((string)o["id"], "Authorization", StringComparison.Ordinal))
.ToList())
{
inputs.Remove(legacy);
}

if (inputs.Count > 0)
{
root["inputs"] = inputs;
}
else if (root["inputs"] != null)
{
root.Remove("inputs");
}

if (unity["headers"] != null)
{
unity.Remove("headers");
}
}

foreach (var prop in urlPropsToRemove)
{
if (unity[prop] != null) unity.Remove(prop);
Expand All @@ -75,6 +143,9 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl
{
unity["type"] = "http";
}

// Set version field
unity["version"] = AssetPathUtility.GetPackageVersion();
}
else
{
Expand Down Expand Up @@ -106,6 +177,9 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl
{
unity["type"] = "stdio";
}

// Set version field
unity["version"] = AssetPathUtility.GetPackageVersion();
}

// Remove type for non-VSCode clients
Expand Down
3 changes: 3 additions & 0 deletions MCPForUnity/Editor/Models/MCPConfigServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ public class McpConfigServer
// VSCode expects a transport type; include only when explicitly set
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public string type;

[JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)]
public string version;
}
}
51 changes: 47 additions & 4 deletions MCPForUnity/Editor/Services/ServerManagementService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Constants;
Expand Down Expand Up @@ -368,11 +369,41 @@ public bool TryGetLocalHttpServerCommand(out string command, out string error)
return false;
}

string args = string.IsNullOrEmpty(fromUrl)
? $"{packageName} --transport http --http-url {httpUrl}"
: $"--from {fromUrl} {packageName} --transport http --http-url {httpUrl}";
var args = new List<string>();

command = $"{uvxPath} {args}";
if (!string.IsNullOrEmpty(fromUrl))
{
args.Add("--from");
args.Add(fromUrl);
}

args.Add(packageName);
args.Add("--transport");
args.Add("http");
args.Add("--http-url");
args.Add(httpUrl);

bool authEnabled = AuthPreferencesUtility.IsAuthEnabled();
if (authEnabled)
{
args.Add("--auth-enabled");

string allowedIps = AuthPreferencesUtility.GetAllowedIps();
if (!string.IsNullOrWhiteSpace(allowedIps))
{
args.Add("--allowed-ips");
args.Add(allowedIps);
}

string apiKey = AuthPreferencesUtility.GetApiKey();
if (!string.IsNullOrEmpty(apiKey))
{
args.Add("--auth-token");
args.Add(apiKey);
}
}

command = $"{QuoteArgument(uvxPath)} {string.Join(" ", args.Select(QuoteArgument))}".Trim();
return true;
}

Expand Down Expand Up @@ -516,5 +547,17 @@ private System.Diagnostics.ProcessStartInfo CreateTerminalProcessStartInfo(strin
};
#endif
}

private static string QuoteArgument(string arg)
{
if (string.IsNullOrEmpty(arg))
{
return "\"\"";
}

bool needsQuotes = arg.IndexOfAny(new[] { ' ', '\"' }) >= 0;
return needsQuotes ? $"\"{arg.Replace("\"", "\\\"")}\"" : arg;
}

}
}
Loading