diff --git a/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs.meta
new file mode 100644
index 000000000..d6c740f35
--- /dev/null
+++ b/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 14a4b9a7f749248d496466c2a3a53e56
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
index 20d97a2f0..0d69cafbc 100644
--- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
+++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
@@ -156,7 +156,17 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
}
else if (!string.IsNullOrEmpty(configuredUrl))
{
- client.configuredTransport = Models.ConfiguredTransport.Http;
+ // 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
{
@@ -173,6 +183,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
}
else if (!string.IsNullOrEmpty(configuredUrl))
{
+ // Match against the active scope's URL
string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl();
matches = UrlsEqual(configuredUrl, expectedUrl);
}
@@ -189,9 +200,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
- // Update transport after rewrite based on current server setting
- bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
- client.configuredTransport = useHttp ? Models.ConfiguredTransport.Http : Models.ConfiguredTransport.Stdio;
+ client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
else
{
@@ -220,9 +229,7 @@ public override void Configure()
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
- // Set transport based on current server setting
- bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
- client.configuredTransport = useHttp ? Models.ConfiguredTransport.Http : Models.ConfiguredTransport.Stdio;
+ client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
else
{
@@ -272,7 +279,16 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
// Determine and set the configured transport type
if (!string.IsNullOrEmpty(url))
{
- client.configuredTransport = Models.ConfiguredTransport.Http;
+ // 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)
{
@@ -286,6 +302,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
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)
@@ -313,9 +330,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
- // Update transport after rewrite based on current server setting
- bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
- client.configuredTransport = useHttp ? Models.ConfiguredTransport.Http : Models.ConfiguredTransport.Stdio;
+ client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
else
{
@@ -344,9 +359,7 @@ public override void Configure()
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
- // Set transport based on current server setting
- bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
- client.configuredTransport = useHttp ? Models.ConfiguredTransport.Http : Models.ConfiguredTransport.Stdio;
+ client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
else
{
@@ -468,9 +481,13 @@ internal McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTran
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 = Models.ConfiguredTransport.Http;
+ client.configuredTransport = HttpEndpointUtility.IsRemoteScope()
+ ? Models.ConfiguredTransport.HttpRemote
+ : Models.ConfiguredTransport.Http;
}
else if (registeredWithStdio)
{
@@ -481,7 +498,7 @@ internal McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTran
client.configuredTransport = Models.ConfiguredTransport.Unknown;
}
- // Check for transport mismatch
+ // Check for transport mismatch (3-way: Stdio, Http, HttpRemote)
bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp);
// For stdio transport, also check package version
@@ -575,7 +592,9 @@ public override void Configure()
public void ConfigureWithCapturedValues(
string projectDir, string claudePath, string pathPrepend,
bool useHttpTransport, string httpUrl,
- string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh)
+ string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh,
+ string apiKey,
+ Models.ConfiguredTransport serverTransport)
{
if (client.status == McpStatus.Configured)
{
@@ -584,7 +603,8 @@ public void ConfigureWithCapturedValues(
else
{
RegisterWithCapturedValues(projectDir, claudePath, pathPrepend,
- useHttpTransport, httpUrl, uvxPath, gitUrl, packageName, shouldForceRefresh);
+ useHttpTransport, httpUrl, uvxPath, gitUrl, packageName, shouldForceRefresh,
+ apiKey, serverTransport);
}
}
@@ -594,7 +614,9 @@ public void ConfigureWithCapturedValues(
private void RegisterWithCapturedValues(
string projectDir, string claudePath, string pathPrepend,
bool useHttpTransport, string httpUrl,
- string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh)
+ string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh,
+ string apiKey,
+ Models.ConfiguredTransport serverTransport)
{
if (string.IsNullOrEmpty(claudePath))
{
@@ -604,7 +626,16 @@ private void RegisterWithCapturedValues(
string args;
if (useHttpTransport)
{
- args = $"mcp add --transport http UnityMCP {httpUrl}";
+ // 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
{
@@ -626,7 +657,7 @@ private void RegisterWithCapturedValues(
McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport.");
client.SetStatus(McpStatus.Configured);
- client.configuredTransport = useHttpTransport ? Models.ConfiguredTransport.Http : Models.ConfiguredTransport.Stdio;
+ client.configuredTransport = serverTransport;
}
///
@@ -664,7 +695,24 @@ private void Register()
if (useHttpTransport)
{
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
- args = $"mcp add --transport http UnityMCP {httpUrl}";
+ // 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
{
@@ -715,7 +763,7 @@ private void Register()
// 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 = useHttpTransport ? Models.ConfiguredTransport.Http : Models.ConfiguredTransport.Stdio;
+ client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
private void Unregister()
@@ -757,8 +805,15 @@ public override string GetManualSnippet()
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}\n\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" +
@@ -790,6 +845,37 @@ public override string GetManualSnippet()
"Restart Claude Code"
};
+ ///
+ /// 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.
+ ///
+ 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();
+ }
+
///
/// Extracts the package source (--from argument value) from claude mcp get output.
/// The output format includes args like: --from "mcpforunityserver==9.0.1"
diff --git a/MCPForUnity/Editor/Constants/AuthConstants.cs b/MCPForUnity/Editor/Constants/AuthConstants.cs
new file mode 100644
index 000000000..76579e642
--- /dev/null
+++ b/MCPForUnity/Editor/Constants/AuthConstants.cs
@@ -0,0 +1,10 @@
+namespace MCPForUnity.Editor.Constants
+{
+ ///
+ /// Protocol-level constants for API key authentication.
+ ///
+ internal static class AuthConstants
+ {
+ internal const string ApiKeyHeader = "X-API-Key";
+ }
+}
diff --git a/MCPForUnity/Editor/Constants/AuthConstants.cs.meta b/MCPForUnity/Editor/Constants/AuthConstants.cs.meta
new file mode 100644
index 000000000..55af6d27f
--- /dev/null
+++ b/MCPForUnity/Editor/Constants/AuthConstants.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 96844bc39e9a94cf18b18f8127f3854f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
index 60f81c1be..a08649e3f 100644
--- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
+++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
@@ -24,6 +24,7 @@ internal static class EditorPrefKeys
internal const string ClaudeCliPathOverride = "MCPForUnity.ClaudeCliPath";
internal const string HttpBaseUrl = "MCPForUnity.HttpUrl";
+ internal const string HttpRemoteBaseUrl = "MCPForUnity.HttpRemoteUrl";
internal const string SessionId = "MCPForUnity.SessionId";
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";
@@ -55,5 +56,7 @@ internal static class EditorPrefKeys
internal const string TelemetryDisabled = "MCPForUnity.TelemetryDisabled";
internal const string CustomerUuid = "MCPForUnity.CustomerUUID";
+
+ internal const string ApiKey = "MCPForUnity.ApiKey";
}
}
diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
index 1e40ba971..938d33c26 100644
--- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
+++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
@@ -71,6 +71,26 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl
if (unity["command"] != null) unity.Remove("command");
if (unity["args"] != null) unity.Remove("args");
+ // Only include API key header for remote-hosted mode
+ if (HttpEndpointUtility.IsRemoteScope())
+ {
+ string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
+ if (!string.IsNullOrEmpty(apiKey))
+ {
+ var headers = new JObject { [AuthConstants.ApiKeyHeader] = apiKey };
+ unity["headers"] = headers;
+ }
+ else
+ {
+ if (unity["headers"] != null) unity.Remove("headers");
+ }
+ }
+ else
+ {
+ // Local HTTP doesn't use API keys; remove any stale headers
+ if (unity["headers"] != null) unity.Remove("headers");
+ }
+
if (isVSCode)
{
unity["type"] = "http";
diff --git a/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs b/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs
index 2fa881bf6..76b7aef11 100644
--- a/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs
+++ b/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs
@@ -1,5 +1,7 @@
using System;
using MCPForUnity.Editor.Constants;
+using MCPForUnity.Editor.Models;
+using MCPForUnity.Editor.Services;
using UnityEditor;
namespace MCPForUnity.Editor.Helpers
@@ -8,38 +10,113 @@ namespace MCPForUnity.Editor.Helpers
/// Helper methods for managing HTTP endpoint URLs used by the MCP bridge.
/// Ensures the stored value is always the base URL (without trailing path),
/// and provides convenience accessors for specific endpoints.
+ ///
+ /// HTTP Local and HTTP Remote use separate EditorPrefs keys so that switching
+ /// between scopes does not overwrite the other scope's URL.
///
public static class HttpEndpointUtility
{
- private const string PrefKey = EditorPrefKeys.HttpBaseUrl;
- private const string DefaultBaseUrl = "http://localhost:8080";
+ private const string LocalPrefKey = EditorPrefKeys.HttpBaseUrl;
+ private const string RemotePrefKey = EditorPrefKeys.HttpRemoteBaseUrl;
+ private const string DefaultLocalBaseUrl = "http://localhost:8080";
+ private const string DefaultRemoteBaseUrl = "";
///
- /// Returns the normalized base URL currently stored in EditorPrefs.
+ /// Returns the normalized base URL for the currently active HTTP scope.
+ /// If the scope is "remote", returns the remote URL; otherwise returns the local URL.
///
public static string GetBaseUrl()
{
- string stored = EditorPrefs.GetString(PrefKey, DefaultBaseUrl);
- return NormalizeBaseUrl(stored);
+ return IsRemoteScope() ? GetRemoteBaseUrl() : GetLocalBaseUrl();
}
///
- /// Saves a user-provided URL after normalizing it to a base form.
+ /// Saves a user-provided URL to the currently active HTTP scope's pref.
///
public static void SaveBaseUrl(string userValue)
{
- string normalized = NormalizeBaseUrl(userValue);
- EditorPrefs.SetString(PrefKey, normalized);
+ if (IsRemoteScope())
+ {
+ SaveRemoteBaseUrl(userValue);
+ }
+ else
+ {
+ SaveLocalBaseUrl(userValue);
+ }
+ }
+
+ ///
+ /// Returns the normalized local HTTP base URL (always reads local pref).
+ ///
+ public static string GetLocalBaseUrl()
+ {
+ string stored = EditorPrefs.GetString(LocalPrefKey, DefaultLocalBaseUrl);
+ return NormalizeBaseUrl(stored, DefaultLocalBaseUrl);
+ }
+
+ ///
+ /// Saves a user-provided URL to the local HTTP pref.
+ ///
+ public static void SaveLocalBaseUrl(string userValue)
+ {
+ string normalized = NormalizeBaseUrl(userValue, DefaultLocalBaseUrl);
+ EditorPrefs.SetString(LocalPrefKey, normalized);
+ }
+
+ ///
+ /// Returns the normalized remote HTTP base URL (always reads remote pref).
+ /// Returns empty string if no remote URL is configured.
+ ///
+ public static string GetRemoteBaseUrl()
+ {
+ string stored = EditorPrefs.GetString(RemotePrefKey, DefaultRemoteBaseUrl);
+ if (string.IsNullOrWhiteSpace(stored))
+ {
+ return DefaultRemoteBaseUrl;
+ }
+ return NormalizeBaseUrl(stored, DefaultRemoteBaseUrl);
+ }
+
+ ///
+ /// Saves a user-provided URL to the remote HTTP pref.
+ ///
+ public static void SaveRemoteBaseUrl(string userValue)
+ {
+ if (string.IsNullOrWhiteSpace(userValue))
+ {
+ EditorPrefs.SetString(RemotePrefKey, DefaultRemoteBaseUrl);
+ return;
+ }
+ string normalized = NormalizeBaseUrl(userValue, DefaultRemoteBaseUrl);
+ EditorPrefs.SetString(RemotePrefKey, normalized);
}
///
- /// Builds the JSON-RPC endpoint used by FastMCP clients (base + /mcp).
+ /// Builds the JSON-RPC endpoint for the currently active scope (base + /mcp).
///
public static string GetMcpRpcUrl()
{
return AppendPathSegment(GetBaseUrl(), "mcp");
}
+ ///
+ /// Builds the local JSON-RPC endpoint (local base + /mcp).
+ ///
+ public static string GetLocalMcpRpcUrl()
+ {
+ return AppendPathSegment(GetLocalBaseUrl(), "mcp");
+ }
+
+ ///
+ /// Builds the remote JSON-RPC endpoint (remote base + /mcp).
+ /// Returns empty string if no remote URL is configured.
+ ///
+ public static string GetRemoteMcpRpcUrl()
+ {
+ string remoteBase = GetRemoteBaseUrl();
+ return string.IsNullOrEmpty(remoteBase) ? string.Empty : AppendPathSegment(remoteBase, "mcp");
+ }
+
///
/// Builds the endpoint used when POSTing custom-tool registration payloads.
///
@@ -48,14 +125,35 @@ public static string GetRegisterToolsUrl()
return AppendPathSegment(GetBaseUrl(), "register-tools");
}
+ ///
+ /// Returns true if the active HTTP transport scope is "remote".
+ ///
+ public static bool IsRemoteScope()
+ {
+ string scope = EditorConfigurationCache.Instance.HttpTransportScope;
+ return string.Equals(scope, "remote", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Returns the that matches the current server-side
+ /// transport selection (Stdio, Http, or HttpRemote).
+ /// Centralises the 3-way determination so callers avoid duplicated logic.
+ ///
+ public static ConfiguredTransport GetCurrentServerTransport()
+ {
+ bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
+ if (!useHttp) return ConfiguredTransport.Stdio;
+ return IsRemoteScope() ? ConfiguredTransport.HttpRemote : ConfiguredTransport.Http;
+ }
+
///
/// Normalizes a URL so that we consistently store just the base (no trailing slash/path).
///
- private static string NormalizeBaseUrl(string value)
+ private static string NormalizeBaseUrl(string value, string defaultUrl)
{
if (string.IsNullOrWhiteSpace(value))
{
- return DefaultBaseUrl;
+ return defaultUrl;
}
string trimmed = value.Trim();
diff --git a/MCPForUnity/Editor/Models/McpStatus.cs b/MCPForUnity/Editor/Models/McpStatus.cs
index 4fb84265e..c23bc819b 100644
--- a/MCPForUnity/Editor/Models/McpStatus.cs
+++ b/MCPForUnity/Editor/Models/McpStatus.cs
@@ -21,9 +21,10 @@ public enum McpStatus
///
public enum ConfiguredTransport
{
- Unknown, // Could not determine transport type
- Stdio, // Client configured for stdio transport
- Http // Client configured for HTTP transport
+ Unknown, // Could not determine transport type
+ Stdio, // Client configured for stdio transport
+ Http, // Client configured for HTTP local transport
+ HttpRemote // Client configured for HTTP remote-hosted transport
}
}
diff --git a/MCPForUnity/Editor/Services/EditorConfigurationCache.cs b/MCPForUnity/Editor/Services/EditorConfigurationCache.cs
index 86b5df95f..40dedd58a 100644
--- a/MCPForUnity/Editor/Services/EditorConfigurationCache.cs
+++ b/MCPForUnity/Editor/Services/EditorConfigurationCache.cs
@@ -53,6 +53,7 @@ public static EditorConfigurationCache Instance
private string _uvxPathOverride;
private string _gitUrlOverride;
private string _httpBaseUrl;
+ private string _httpRemoteBaseUrl;
private string _claudeCliPathOverride;
private string _httpTransportScope;
private int _unitySocketPort;
@@ -94,11 +95,17 @@ public static EditorConfigurationCache Instance
public string GitUrlOverride => _gitUrlOverride;
///
- /// HTTP base URL for the MCP server.
+ /// HTTP base URL for the local MCP server.
/// Default: empty string
///
public string HttpBaseUrl => _httpBaseUrl;
+ ///
+ /// HTTP base URL for the remote-hosted MCP server.
+ /// Default: empty string
+ ///
+ public string HttpRemoteBaseUrl => _httpRemoteBaseUrl;
+
///
/// Custom path override for Claude CLI executable.
/// Default: empty string (auto-detect)
@@ -135,6 +142,7 @@ public void Refresh()
_uvxPathOverride = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
_gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, string.Empty);
_httpBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpBaseUrl, string.Empty);
+ _httpRemoteBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpRemoteBaseUrl, string.Empty);
_claudeCliPathOverride = EditorPrefs.GetString(EditorPrefKeys.ClaudeCliPathOverride, string.Empty);
_httpTransportScope = EditorPrefs.GetString(EditorPrefKeys.HttpTransportScope, string.Empty);
_unitySocketPort = EditorPrefs.GetInt(EditorPrefKeys.UnitySocketPort, 0);
@@ -234,6 +242,20 @@ public void SetHttpBaseUrl(string value)
}
}
+ ///
+ /// Set HttpRemoteBaseUrl and update cache + EditorPrefs atomically.
+ ///
+ public void SetHttpRemoteBaseUrl(string value)
+ {
+ value = value ?? string.Empty;
+ if (_httpRemoteBaseUrl != value)
+ {
+ _httpRemoteBaseUrl = value;
+ EditorPrefs.SetString(EditorPrefKeys.HttpRemoteBaseUrl, value);
+ OnConfigurationChanged?.Invoke(nameof(HttpRemoteBaseUrl));
+ }
+ }
+
///
/// Set ClaudeCliPathOverride and update cache + EditorPrefs atomically.
///
@@ -304,6 +326,9 @@ public void InvalidateKey(string keyName)
case nameof(HttpBaseUrl):
_httpBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpBaseUrl, string.Empty);
break;
+ case nameof(HttpRemoteBaseUrl):
+ _httpRemoteBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpRemoteBaseUrl, string.Empty);
+ break;
case nameof(ClaudeCliPathOverride):
_claudeCliPathOverride = EditorPrefs.GetString(EditorPrefKeys.ClaudeCliPathOverride, string.Empty);
break;
diff --git a/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs b/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs
index 47791aa99..47b46755b 100644
--- a/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs
+++ b/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs
@@ -30,7 +30,7 @@ public bool TryBuildCommand(out string fileName, out string arguments, out strin
return false;
}
- string httpUrl = HttpEndpointUtility.GetBaseUrl();
+ string httpUrl = HttpEndpointUtility.GetLocalBaseUrl();
if (!IsLocalUrl(httpUrl))
{
error = $"The configured URL ({httpUrl}) is not a local address. Local server launch only works for localhost.";
diff --git a/MCPForUnity/Editor/Services/ServerManagementService.cs b/MCPForUnity/Editor/Services/ServerManagementService.cs
index d67dfada6..1df3384fb 100644
--- a/MCPForUnity/Editor/Services/ServerManagementService.cs
+++ b/MCPForUnity/Editor/Services/ServerManagementService.cs
@@ -1,7 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Collections.Generic;
using System.Net.Sockets;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
@@ -158,7 +158,7 @@ public bool ClearUvxCache()
if (success)
{
- McpLog.Debug($"uv cache cleared successfully: {stdout}");
+ McpLog.Info($"uv cache cleared successfully: {stdout}");
return true;
}
string combinedOutput = string.Join(
@@ -253,7 +253,7 @@ public bool StartLocalHttpServer()
// If the port is still occupied, don't start and explain why (avoid confusing "refusing to stop" warnings).
try
{
- string httpUrl = HttpEndpointUtility.GetBaseUrl();
+ string httpUrl = HttpEndpointUtility.GetLocalBaseUrl();
if (Uri.TryCreate(httpUrl, UriKind.Absolute, out var uri) && uri.Port > 0)
{
var remaining = GetListeningProcessIdsForPort(uri.Port);
@@ -274,7 +274,7 @@ public bool StartLocalHttpServer()
// Note: Dev mode cache-busting is handled by `uvx --no-cache --refresh` in the generated command.
// Create a per-launch token + pidfile path so Stop can be deterministic without relying on port/PID heuristics.
- string baseUrlForPid = HttpEndpointUtility.GetBaseUrl();
+ string baseUrlForPid = HttpEndpointUtility.GetLocalBaseUrl();
Uri.TryCreate(baseUrlForPid, UriKind.Absolute, out var uriForPid);
int portForPid = uriForPid?.Port ?? 0;
string instanceToken = Guid.NewGuid().ToString("N");
@@ -350,7 +350,7 @@ public bool StopManagedLocalHttpServer()
int port = 0;
if (!TryGetPortFromPidFilePath(pidFilePath, out port) || port <= 0)
{
- string baseUrl = HttpEndpointUtility.GetBaseUrl();
+ string baseUrl = HttpEndpointUtility.GetLocalBaseUrl();
if (IsLocalUrl(baseUrl)
&& Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri)
&& uri.Port > 0)
@@ -371,7 +371,7 @@ public bool IsLocalHttpServerRunning()
{
try
{
- string httpUrl = HttpEndpointUtility.GetBaseUrl();
+ string httpUrl = HttpEndpointUtility.GetLocalBaseUrl();
if (!IsLocalUrl(httpUrl))
{
return false;
@@ -433,7 +433,7 @@ public bool IsLocalHttpServerReachable()
{
try
{
- string httpUrl = HttpEndpointUtility.GetBaseUrl();
+ string httpUrl = HttpEndpointUtility.GetLocalBaseUrl();
if (!IsLocalUrl(httpUrl))
{
return false;
@@ -500,7 +500,7 @@ private static bool TryConnectToLocalPort(string host, int port, int timeoutMs)
private bool StopLocalHttpServerInternal(bool quiet, int? portOverride = null, bool allowNonLocalUrl = false)
{
- string httpUrl = HttpEndpointUtility.GetBaseUrl();
+ string httpUrl = HttpEndpointUtility.GetLocalBaseUrl();
if (!allowNonLocalUrl && !IsLocalUrl(httpUrl))
{
if (!quiet)
@@ -665,14 +665,14 @@ private bool StopLocalHttpServerInternal(bool quiet, int? portOverride = null, b
// fall back to a looser check to avoid leaving orphaned servers after domain reload.
if (TryGetUnixProcessArgs(storedPid, out var storedArgsLowerNow))
{
- // Never kill Unity/Hub.
- // Note: "mcp-for-unity" includes "unity", so detect MCP indicators first.
- bool storedMentionsMcp = storedArgsLowerNow.Contains("mcp-for-unity")
- || storedArgsLowerNow.Contains("mcp_for_unity")
- || storedArgsLowerNow.Contains("mcpforunity");
- if (storedArgsLowerNow.Contains("unityhub")
- || storedArgsLowerNow.Contains("unity hub")
- || (storedArgsLowerNow.Contains("unity") && !storedMentionsMcp))
+ // Never kill Unity/Hub.
+ // Note: "mcp-for-unity" includes "unity", so detect MCP indicators first.
+ bool storedMentionsMcp = storedArgsLowerNow.Contains("mcp-for-unity")
+ || storedArgsLowerNow.Contains("mcp_for_unity")
+ || storedArgsLowerNow.Contains("mcpforunity");
+ if (storedArgsLowerNow.Contains("unityhub")
+ || storedArgsLowerNow.Contains("unity hub")
+ || (storedArgsLowerNow.Contains("unity") && !storedMentionsMcp))
{
if (!quiet)
{
@@ -836,7 +836,7 @@ private bool TryGetLocalHttpServerCommandParts(out string fileName, out string a
///
public bool IsLocalUrl()
{
- string httpUrl = HttpEndpointUtility.GetBaseUrl();
+ string httpUrl = HttpEndpointUtility.GetLocalBaseUrl();
return IsLocalUrl(httpUrl);
}
diff --git a/MCPForUnity/Editor/Services/Transport/TransportManager.cs b/MCPForUnity/Editor/Services/Transport/TransportManager.cs
index 44f53ce0d..1204e7014 100644
--- a/MCPForUnity/Editor/Services/Transport/TransportManager.cs
+++ b/MCPForUnity/Editor/Services/Transport/TransportManager.cs
@@ -67,7 +67,7 @@ public async Task StartAsync(TransportMode mode)
{
McpLog.Warn($"Error while stopping transport {client.TransportName}: {ex.Message}");
}
- UpdateState(mode, TransportState.Disconnected(client.TransportName, "Failed to start"));
+ UpdateState(mode, TransportState.Disconnected(client.TransportName, client.State?.Error ?? "Failed to start"));
return false;
}
diff --git a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
index 0b6c4aafc..856577c06 100644
--- a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
+++ b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
@@ -6,6 +6,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Services.Transport;
@@ -56,6 +57,7 @@ public class WebSocketTransportClient : IMcpTransportClient, IDisposable
private volatile bool _isConnected;
private int _isReconnectingFlag;
private TransportState _state = TransportState.Disconnected(TransportDisplayName, "Transport not started");
+ private string _apiKey;
private bool _disposed;
public WebSocketTransportClient(IToolDiscoveryService toolDiscoveryService = null)
@@ -80,6 +82,9 @@ public async Task StartAsync()
_projectName = ProjectIdentityUtility.GetProjectName();
_projectHash = ProjectIdentityUtility.GetProjectHash();
_unityVersion = Application.unityVersion;
+ _apiKey = HttpEndpointUtility.IsRemoteScope()
+ ? EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty)
+ : string.Empty;
// Get project root path (strip /Assets from dataPath) for focus nudging
string dataPath = Application.dataPath;
@@ -214,13 +219,21 @@ private async Task EstablishConnectionAsync(CancellationToken token)
_socket = new ClientWebSocket();
_socket.Options.KeepAliveInterval = _socketKeepAliveInterval;
+ // Add API key header if configured (for remote-hosted mode)
+ if (!string.IsNullOrEmpty(_apiKey))
+ {
+ _socket.Options.SetRequestHeader(AuthConstants.ApiKeyHeader, _apiKey);
+ }
+
try
{
await _socket.ConnectAsync(_endpointUri, connectionToken).ConfigureAwait(false);
}
catch (Exception ex)
{
- McpLog.Error($"[WebSocket] Connection failed: {ex.Message}");
+ string errorMsg = "Connection failed. Check that the server URL is correct, the server is running, and your API key (if required) is valid.";
+ McpLog.Error($"[WebSocket] {errorMsg} (Detail: {ex.Message})");
+ _state = TransportState.Disconnected(TransportDisplayName, errorMsg);
return false;
}
@@ -232,7 +245,9 @@ private async Task EstablishConnectionAsync(CancellationToken token)
}
catch (Exception ex)
{
- McpLog.Error($"[WebSocket] Registration failed: {ex.Message}");
+ string regMsg = $"Registration with server failed: {ex.Message}";
+ McpLog.Error($"[WebSocket] {regMsg}");
+ _state = TransportState.Disconnected(TransportDisplayName, regMsg);
return false;
}
diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
index 91fb26a65..0433a824a 100644
--- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
+++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
@@ -275,6 +275,7 @@ private void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
bool shouldForceRefresh = AssetPathUtility.ShouldForceUvxRefresh();
+ string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
// Compute pathPrepend on main thread
string pathPrepend = null;
@@ -296,10 +297,12 @@ private void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
{
if (client is ClaudeCliMcpConfigurator cliConfigurator)
{
+ var serverTransport = HttpEndpointUtility.GetCurrentServerTransport();
cliConfigurator.ConfigureWithCapturedValues(
projectDir, claudePath, pathPrepend,
useHttpTransport, httpUrl,
- uvxPath, gitUrl, packageName, shouldForceRefresh);
+ uvxPath, gitUrl, packageName, shouldForceRefresh,
+ apiKey, serverTransport);
}
return (success: true, error: (string)null);
}
@@ -525,12 +528,11 @@ private void ApplyStatusToUi(IMcpClientConfigurator client, bool showChecking =
return;
}
- // Check for transport mismatch
+ // Check for transport mismatch (3-way: Stdio, Http, HttpRemote)
bool hasTransportMismatch = false;
if (client.ConfiguredTransport != ConfiguredTransport.Unknown)
{
- bool serverUsesHttp = EditorConfigurationCache.Instance.UseHttpTransport;
- ConfiguredTransport serverTransport = serverUsesHttp ? ConfiguredTransport.Http : ConfiguredTransport.Stdio;
+ ConfiguredTransport serverTransport = HttpEndpointUtility.GetCurrentServerTransport();
hasTransportMismatch = client.ConfiguredTransport != serverTransport;
}
diff --git a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
index 23e35f15c..36f564749 100644
--- a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
+++ b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
@@ -45,6 +45,13 @@ private enum TransportProtocol
private Label connectionStatusLabel;
private Button connectionToggleButton;
+ // API Key UI Elements (for remote-hosted mode)
+ private VisualElement apiKeyRow;
+ private TextField apiKeyField;
+ private Button getApiKeyButton;
+ private Button clearApiKeyButton;
+ private string cachedLoginUrl;
+
private bool connectionToggleInProgress;
private bool httpServerToggleInProgress;
private Task verificationTask;
@@ -93,6 +100,12 @@ private void CacheUIElements()
statusIndicator = Root.Q("status-indicator");
connectionStatusLabel = Root.Q