Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
363a349
Disable the gloabl default to first session when hosting remotely
msanatan Jan 24, 2026
1490d47
Remove calls to /plugin/sessions
msanatan Jan 24, 2026
661530e
Disable CLI routes when running in remote hosted mode
msanatan Jan 24, 2026
f88c3e1
Update server README
msanatan Jan 24, 2026
ce2e755
Merge branch 'beta' into remote-server-auth
msanatan Jan 26, 2026
2aebd45
Merge branch 'beta' into remote-server-auth
msanatan Jan 27, 2026
45f8cf1
feat: add API key authentication support for remote-hosted HTTP trans…
msanatan Jan 28, 2026
f1ab880
feat: add environment variable support for HTTP remote hosted mode
msanatan Jan 28, 2026
bff2752
feat: add user isolation enforcement for remote-hosted mode session l…
msanatan Jan 28, 2026
7617d08
feat: add comprehensive integration tests for API key authentication
msanatan Jan 28, 2026
d4273d9
Merge branch 'beta' into remote-server-auth
msanatan Jan 28, 2026
ba63c6d
test: add autouse fixture to restore config state after startup valid…
msanatan Jan 28, 2026
e8aac14
feat: skip user_id resolution in non-remote-hosted mode
msanatan Jan 28, 2026
dccc3e2
test: add missing mock attributes to instance routing tests
msanatan Jan 28, 2026
5cbeb86
Fix broken telemetry test
msanatan Jan 29, 2026
9ed3703
Add comprehensive API key authentication documentation
msanatan Jan 29, 2026
76d4472
Add remote-hosted mode and API key authentication documentation to se…
msanatan Jan 29, 2026
3352325
Update reference doc for Docker Hub
msanatan Jan 29, 2026
e3d19f8
Specify exception being caught
msanatan Jan 29, 2026
5a554fc
Ensure caplog handler cleanup in telemetry queue worker test
msanatan Jan 29, 2026
40440f7
Use NoUnitySessionError instead of RuntimeError in session isolation …
msanatan Jan 29, 2026
e4764a6
Remove unusued monkeypatch arg
msanatan Jan 29, 2026
f1c71e6
Use more obviously fake API keys
msanatan Jan 29, 2026
1ec4815
Reject connections when ApiKeyService is not initialized in remote-ho…
msanatan Jan 29, 2026
dbeacfd
Accept "on" for UNITY_MCP_HTTP_REMOTE_HOSTED env var
msanatan Jan 29, 2026
60422cf
Invalidate cached login URL when HTTP base URL changes
msanatan Jan 29, 2026
a8c06ac
Pass API key as parameter instead of reading from EditorPrefs in Regi…
msanatan Jan 29, 2026
f626afc
Cache API key in field instead of reading from EditorPrefs on each re…
msanatan Jan 29, 2026
be1e930
Align markdown table formatting in remote server auth documentation
msanatan Jan 29, 2026
053115e
Merge branch 'beta' into remote-server-auth
msanatan Jan 29, 2026
1aa9abc
Minor fixes
msanatan Jan 29, 2026
f40fab9
security: Sanitize API key values in shell commands and fix minor issues
msanatan Jan 29, 2026
14e21fc
Consolidate duplicate instance selection error messages into Instance…
msanatan Jan 29, 2026
02e726b
Replace hardcoded "X-API-Key" strings with AuthConstants.ApiKeyHeader…
msanatan Jan 29, 2026
532a61e
Fix imports
msanatan Jan 29, 2026
95b2b83
Filter session listing by user_id in all code paths to prevent cross-…
msanatan Jan 29, 2026
535c8e1
Consolidate get_session_id_by_hash methods into single method with op…
msanatan Jan 29, 2026
d8ce7d3
Add environment variable support for project-scoped-tools flag [skip ci]
msanatan Jan 29, 2026
1ecd008
Fix Python tests
msanatan Jan 29, 2026
2bde4cc
Update validation logic to only require API key validation URL when b…
msanatan Jan 29, 2026
3014729
Update Server/src/main.py
msanatan Jan 29, 2026
72318b3
Refactor HTTP transport configuration to support separate local and r…
msanatan Jan 30, 2026
a7051df
Only include API key headers in HTTP/WebSocket configuration when in …
msanatan Jan 30, 2026
d02e419
Hide Manual Server Launch foldout when not in HTTP Local mode
msanatan Jan 30, 2026
bb5f764
Fix failing test
msanatan Jan 30, 2026
c130510
Improve error messaging and API key validation for HTTP Remote transport
msanatan Jan 30, 2026
00891a6
Merge branch 'beta' into remote-server-auth
msanatan Jan 30, 2026
9cce578
Add missing .meta file
msanatan Jan 30, 2026
16f5235
Store transport mode in ServerConfig instead of environment variable
msanatan Jan 30, 2026
0f237b3
Add autouse fixture to restore global config state between tests
msanatan Jan 30, 2026
39acb87
Fix startup
msanatan Jan 30, 2026
eb448f8
Replace _current_transport() calls with direct config.transport_mode …
msanatan Jan 30, 2026
652f1de
Minor cleanup
msanatan Jan 30, 2026
0187afe
Add integration tests for HTTP transport authentication behavior
msanatan Jan 30, 2026
8aefea3
Add smoke tests for transport routing paths across HTTP local, HTTP r…
msanatan Jan 30, 2026
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

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

134 changes: 110 additions & 24 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
}
Expand All @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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
Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
}
}

Expand All @@ -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))
{
Expand All @@ -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
{
Expand All @@ -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;
}

/// <summary>
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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" +
Expand Down Expand Up @@ -790,6 +845,37 @@ public override string GetManualSnippet()
"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"
Expand Down
10 changes: 10 additions & 0 deletions MCPForUnity/Editor/Constants/AuthConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Protocol-level constants for API key authentication.
/// </summary>
internal static class AuthConstants
{
internal const string ApiKeyHeader = "X-API-Key";
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Constants/AuthConstants.cs.meta

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

3 changes: 3 additions & 0 deletions MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
}
}
20 changes: 20 additions & 0 deletions MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading