From 363a349973abb4e88d8baa797bdf6d01871cd823 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 24 Jan 2026 00:20:06 -0400 Subject: [PATCH 01/50] Disable the gloabl default to first session when hosting remotely --- Server/src/core/config.py | 3 ++ Server/src/main.py | 15 +++++++++ Server/src/transport/plugin_hub.py | 33 +++++++++++++++---- .../transport/unity_instance_middleware.py | 4 +++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/Server/src/core/config.py b/Server/src/core/config.py index 3a97c263e..2f1fd239c 100644 --- a/Server/src/core/config.py +++ b/Server/src/core/config.py @@ -15,6 +15,9 @@ class ServerConfig: unity_port: int = 6400 mcp_port: int = 6500 + # HTTP transport behaviour + http_remote_hosted: bool = False + # Connection settings connection_timeout: float = 30.0 buffer_size: int = 16 * 1024 * 1024 # 16MB buffer diff --git a/Server/src/main.py b/Server/src/main.py index 7755fc675..bf73e7a5d 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -365,6 +365,14 @@ async def cli_command_route(request: Request) -> JSONResponse: status_code=404, ) else: + if config.http_remote_hosted: + return JSONResponse( + { + "success": False, + "error": "unity_instance is required. Use /api/instances or mcpforunity://instances to pick a Name@hash (or hash).", + }, + status_code=400, + ) # No specific unity_instance requested: use first available session session_id = next(iter(sessions.sessions.keys())) @@ -491,6 +499,11 @@ def main(): help="HTTP server port (overrides URL port). " "Overrides UNITY_MCP_HTTP_PORT environment variable." ) + parser.add_argument( + "--http-remote-hosted", + action="store_true", + help="Treat HTTP transport as remotely hosted (forces explicit Unity instance selection)." + ) parser.add_argument( "--unity-instance-token", type=str, @@ -515,6 +528,8 @@ def main(): args = parser.parse_args() + config.http_remote_hosted = bool(args.http_remote_hosted) + # Set environment variables from command line args if args.default_instance: os.environ["UNITY_MCP_DEFAULT_INSTANCE"] = args.default_instance diff --git a/Server/src/transport/plugin_hub.py b/Server/src/transport/plugin_hub.py index b0446caff..ebd6e41b9 100644 --- a/Server/src/transport/plugin_hub.py +++ b/Server/src/transport/plugin_hub.py @@ -410,24 +410,32 @@ async def _resolve_session_id(cls, unity_instance: str | None) -> str: else: target_hash = unity_instance - async def _try_once() -> tuple[str | None, int]: + async def _try_once() -> tuple[str | None, int, bool]: + explicit_required = config.http_remote_hosted # Prefer a specific Unity instance if one was requested if target_hash: session_id = await cls._registry.get_session_id_by_hash(target_hash) sessions = await cls._registry.list_sessions() - return session_id, len(sessions) + return session_id, len(sessions), explicit_required # No target provided: determine if we can auto-select sessions = await cls._registry.list_sessions() count = len(sessions) if count == 0: - return None, count + return None, count, explicit_required + if explicit_required: + return None, count, explicit_required if count == 1: - return next(iter(sessions.keys())), count + return next(iter(sessions.keys())), count, explicit_required # Multiple sessions but no explicit target is ambiguous - return None, count + return None, count, explicit_required - session_id, session_count = await _try_once() + session_id, session_count, explicit_required = await _try_once() + if session_id is None and explicit_required and not target_hash and session_count > 0: + raise RuntimeError( + "Unity instance selection is required. " + "Call set_active_instance with Name@hash from mcpforunity://instances." + ) deadline = time.monotonic() + max_wait_s wait_started = None @@ -439,6 +447,11 @@ async def _try_once() -> tuple[str | None, int]: "Multiple Unity instances are connected. " "Call set_active_instance with Name@hash from mcpforunity://instances." ) + if session_id is None and explicit_required and not target_hash and session_count > 0: + raise RuntimeError( + "Unity instance selection is required. " + "Call set_active_instance with Name@hash from mcpforunity://instances." + ) if wait_started is None: wait_started = time.monotonic() logger.debug( @@ -447,7 +460,7 @@ async def _try_once() -> tuple[str | None, int]: max_wait_s, ) await asyncio.sleep(sleep_seconds) - session_id, session_count = await _try_once() + session_id, session_count, explicit_required = await _try_once() if session_id is not None and wait_started is not None: logger.debug( @@ -461,6 +474,12 @@ async def _try_once() -> tuple[str | None, int]: "Call set_active_instance with Name@hash from mcpforunity://instances." ) + if session_id is None and explicit_required and not target_hash and session_count > 0: + raise RuntimeError( + "Unity instance selection is required. " + "Call set_active_instance with Name@hash from mcpforunity://instances." + ) + if session_id is None: logger.warning( "No Unity plugin reconnected within %.2fs (instance=%s)", diff --git a/Server/src/transport/unity_instance_middleware.py b/Server/src/transport/unity_instance_middleware.py index 4866ad0b9..527d20db7 100644 --- a/Server/src/transport/unity_instance_middleware.py +++ b/Server/src/transport/unity_instance_middleware.py @@ -9,6 +9,7 @@ from fastmcp.server.middleware import Middleware, MiddlewareContext +from core.config import config from transport.plugin_hub import PluginHub logger = logging.getLogger("mcp-for-unity-server") @@ -96,6 +97,9 @@ async def _maybe_autoselect_instance(self, ctx) -> str | None: from transport.unity_transport import _current_transport transport = _current_transport() + # This implicit behavior works well for solo-users, but is dangerous for multi-user setups + if transport == "http" and config.http_remote_hosted: + return None if PluginHub.is_configured(): try: sessions_data = await PluginHub.get_sessions() From 1490d47bf5ced35bf5199ee4221ac673c5202687 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 24 Jan 2026 00:24:12 -0400 Subject: [PATCH 02/50] Remove calls to /plugin/sessions The newer /api/instances covers that data, and we want to remove these "expose all" endpoints --- Server/src/cli/utils/connection.py | 60 ++++++++++++++---------------- Server/tests/test_cli.py | 32 ---------------- 2 files changed, 28 insertions(+), 64 deletions(-) diff --git a/Server/src/cli/utils/connection.py b/Server/src/cli/utils/connection.py index 33924e87b..a8a92eeb8 100644 --- a/Server/src/cli/utils/connection.py +++ b/Server/src/cli/utils/connection.py @@ -152,38 +152,34 @@ async def list_unity_instances(config: Optional[CLIConfig] = None) -> Dict[str, """ cfg = config or get_config() - # Try the new /api/instances endpoint first, fall back to /plugin/sessions - urls_to_try = [ - f"http://{cfg.host}:{cfg.port}/api/instances", - f"http://{cfg.host}:{cfg.port}/plugin/sessions", - ] - - async with httpx.AsyncClient() as client: - for url in urls_to_try: - try: - response = await client.get(url, timeout=10) - if response.status_code == 200: - data = response.json() - # Normalize response format - if "instances" in data: - return data - elif "sessions" in data: - # Convert sessions format to instances format - instances = [] - for session_id, details in data["sessions"].items(): - instances.append({ - "session_id": session_id, - "project": details.get("project", "Unknown"), - "hash": details.get("hash", ""), - "unity_version": details.get("unity_version", "Unknown"), - "connected_at": details.get("connected_at", ""), - }) - return {"success": True, "instances": instances} - except Exception: - continue - - raise UnityConnectionError( - "Failed to list Unity instances: No working endpoint found") + url = f"http://{cfg.host}:{cfg.port}/api/instances" + + try: + async with httpx.AsyncClient() as client: + response = await client.get(url, timeout=10) + response.raise_for_status() + data = response.json() + if "instances" in data: + return data + except httpx.ConnectError as e: + raise UnityConnectionError( + f"Cannot connect to Unity MCP server at {cfg.host}:{cfg.port}. " + f"Make sure the server is running and Unity is connected.\n" + f"Error: {e}" + ) + except httpx.TimeoutException: + raise UnityConnectionError( + "Connection to Unity timed out while listing instances. " + "Unity may be busy or unresponsive." + ) + except httpx.HTTPStatusError as e: + raise UnityConnectionError( + f"HTTP error from server: {e.response.status_code} - {e.response.text}" + ) + except Exception as e: + raise UnityConnectionError(f"Unexpected error: {e}") + + raise UnityConnectionError("Failed to list Unity instances") def run_list_instances(config: Optional[CLIConfig] = None) -> Dict[str, Any]: diff --git a/Server/tests/test_cli.py b/Server/tests/test_cli.py index 9e1bb47da..fbcd98ee1 100644 --- a/Server/tests/test_cli.py +++ b/Server/tests/test_cli.py @@ -65,21 +65,6 @@ def mock_instances_response(): } -@pytest.fixture -def mock_sessions_response(): - """Mock plugin sessions response (legacy format).""" - return { - "sessions": { - "test-session-123": { - "project": "TestProject", - "hash": "abc123def456", - "unity_version": "2022.3.10f1", - "connected_at": "2024-01-01T00:00:00Z", - } - } - } - - # ============================================================================= # Config Tests # ============================================================================= @@ -246,23 +231,6 @@ async def test_send_command_connection_error(self): with pytest.raises(UnityConnectionError): await send_command("test_command", {}) - @pytest.mark.asyncio - async def test_list_instances_from_sessions(self, mock_sessions_response): - """Test listing instances from /plugin/sessions endpoint.""" - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.json.return_value = mock_sessions_response - - with patch("httpx.AsyncClient") as mock_client: - # First call (api/instances) returns 404, second (plugin/sessions) succeeds - mock_get = AsyncMock(return_value=mock_response) - mock_client.return_value.__aenter__.return_value.get = mock_get - - result = await list_unity_instances() - assert result["success"] is True - assert len(result["instances"]) == 1 - assert result["instances"][0]["project"] == "TestProject" - # ============================================================================= # CLI Command Tests From 661530e128fb7941a72effa6992abe5fc15d12b1 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 24 Jan 2026 00:25:04 -0400 Subject: [PATCH 03/50] Disable CLI routes when running in remote hosted mode --- Server/src/main.py | 151 +++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 81 deletions(-) diff --git a/Server/src/main.py b/Server/src/main.py index bf73e7a5d..5efb085ed 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -325,87 +325,76 @@ async def health_http(_: Request) -> JSONResponse: "message": "MCP for Unity server is running" }) - @mcp.custom_route("/api/command", methods=["POST"]) - async def cli_command_route(request: Request) -> JSONResponse: - """REST endpoint for CLI commands to Unity.""" - try: - body = await request.json() - - command_type = body.get("type") - params = body.get("params", {}) - unity_instance = body.get("unity_instance") - - if not command_type: - return JSONResponse({"success": False, "error": "Missing 'type' field"}, status_code=400) - - # Get available sessions - sessions = await PluginHub.get_sessions() - if not sessions.sessions: - return JSONResponse({ - "success": False, - "error": "No Unity instances connected. Make sure Unity is running with MCP plugin." - }, status_code=503) - - # Find target session - session_id = None - if unity_instance: - # Try to match by hash or project name - for sid, details in sessions.sessions.items(): - if details.hash == unity_instance or details.project == unity_instance: - session_id = sid - break - - # If a specific unity_instance was requested but not found, return an error - if not session_id: - return JSONResponse( - { - "success": False, - "error": f"Unity instance '{unity_instance}' not found", - }, - status_code=404, - ) - else: - if config.http_remote_hosted: - return JSONResponse( - { - "success": False, - "error": "unity_instance is required. Use /api/instances or mcpforunity://instances to pick a Name@hash (or hash).", - }, - status_code=400, - ) - # No specific unity_instance requested: use first available session - session_id = next(iter(sessions.sessions.keys())) - - # Send command to Unity - result = await PluginHub.send_command(session_id, command_type, params) - return JSONResponse(result) - - except Exception as e: - logger.error(f"CLI command error: {e}") - return JSONResponse({"success": False, "error": str(e)}, status_code=500) - - @mcp.custom_route("/api/instances", methods=["GET"]) - async def cli_instances_route(_: Request) -> JSONResponse: - """REST endpoint to list connected Unity instances.""" - try: - sessions = await PluginHub.get_sessions() - instances = [] - for session_id, details in sessions.sessions.items(): - instances.append({ - "session_id": session_id, - "project": details.project, - "hash": details.hash, - "unity_version": details.unity_version, - "connected_at": details.connected_at, - }) - return JSONResponse({"success": True, "instances": instances}) - except Exception as e: - return JSONResponse({"success": False, "error": str(e)}, status_code=500) - - @mcp.custom_route("/plugin/sessions", methods=["GET"]) - async def plugin_sessions_route(_: Request) -> JSONResponse: - data = await PluginHub.get_sessions() - return JSONResponse(data.model_dump()) + # Only expose CLI routes if running locally (not in remote hosted mode) + if not config.http_remote_hosted: + @mcp.custom_route("/api/command", methods=["POST"]) + async def cli_command_route(request: Request) -> JSONResponse: + """REST endpoint for CLI commands to Unity.""" + try: + body = await request.json() + + command_type = body.get("type") + params = body.get("params", {}) + unity_instance = body.get("unity_instance") + + if not command_type: + return JSONResponse({"success": False, "error": "Missing 'type' field"}, status_code=400) + + # Get available sessions + sessions = await PluginHub.get_sessions() + if not sessions.sessions: + return JSONResponse({ + "success": False, + "error": "No Unity instances connected. Make sure Unity is running with MCP plugin." + }, status_code=503) + + # Find target session + session_id = None + if unity_instance: + # Try to match by hash or project name + for sid, details in sessions.sessions.items(): + if details.hash == unity_instance or details.project == unity_instance: + session_id = sid + break + + # If a specific unity_instance was requested but not found, return an error + if not session_id: + return JSONResponse( + { + "success": False, + "error": f"Unity instance '{unity_instance}' not found", + }, + status_code=404, + ) + else: + # No specific unity_instance requested: use first available session + session_id = next(iter(sessions.sessions.keys())) + + # Send command to Unity + result = await PluginHub.send_command(session_id, command_type, params) + return JSONResponse(result) + + except Exception as e: + logger.error(f"CLI command error: {e}") + return JSONResponse({"success": False, "error": str(e)}, status_code=500) + + @mcp.custom_route("/api/instances", methods=["GET"]) + async def cli_instances_route(_: Request) -> JSONResponse: + """REST endpoint to list connected Unity instances.""" + try: + sessions = await PluginHub.get_sessions() + instances = [] + for session_id, details in sessions.sessions.items(): + instances.append({ + "session_id": session_id, + "project": details.project, + "hash": details.hash, + "unity_version": details.unity_version, + "connected_at": details.connected_at, + }) + return JSONResponse({"success": True, "instances": instances}) + except Exception as e: + return JSONResponse({"success": False, "error": str(e)}, status_code=500) # Initialize and register middleware for session-based Unity instance routing # Using the singleton getter ensures we use the same instance everywhere From f88c3e1ce9060181a78e4b7b814168efd44e7ad1 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 24 Jan 2026 00:43:26 -0400 Subject: [PATCH 04/50] Update server README --- Server/README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/Server/README.md b/Server/README.md index 68855400b..49c8715d8 100644 --- a/Server/README.md +++ b/Server/README.md @@ -113,12 +113,66 @@ uv run src/main.py --transport stdio ## Configuration -The server connects to Unity Editor automatically when both are running. No additional configuration needed. +The server connects to Unity Editor automatically when both are running. Most users do not need to change any settings. -**Environment Variables:** +### CLI options -- `DISABLE_TELEMETRY=true` - Opt out of anonymous usage analytics -- `LOG_LEVEL=DEBUG` - Enable detailed logging (default: INFO) +These options apply to the `mcp-for-unity` command (whether run via `uvx`, Docker, or `python src/main.py`). + +- `--transport {stdio,http}` - Transport protocol (default: `stdio`) +- `--http-url URL` - Base URL used to derive host/port defaults (default: `http://localhost:8080`) +- `--http-host HOST` - Override HTTP bind host (overrides URL host) +- `--http-port PORT` - Override HTTP bind port (overrides URL port) +- `--http-remote-hosted` - Treat HTTP transport as remotely hosted + - Disables local/CLI-only HTTP routes (`/api/command`, `/api/instances`) + - Forces explicit Unity instance selection for MCP tool/resource calls +- `--default-instance INSTANCE` - Default Unity instance to target (project name, hash, or `Name@hash`) +- `--project-scoped-tools` - Keep custom tools scoped to the active Unity project and enable the custom tools resource +- `--unity-instance-token TOKEN` - Optional per-launch token set by Unity for deterministic lifecycle management +- `--pidfile PATH` - Optional path where the server writes its PID on startup (used by Unity-managed terminal launches) + +### Environment variables + +- `UNITY_MCP_TRANSPORT` - Transport protocol: `stdio` or `http` +- `UNITY_MCP_HTTP_URL` - HTTP server URL (default: `http://localhost:8080`) +- `UNITY_MCP_HTTP_HOST` - HTTP bind host (overrides URL host) +- `UNITY_MCP_HTTP_PORT` - HTTP bind port (overrides URL port) +- `UNITY_MCP_DEFAULT_INSTANCE` - Default Unity instance to target (project name, hash, or `Name@hash`) +- `UNITY_MCP_SKIP_STARTUP_CONNECT=1` - Skip initial Unity connection attempt on startup + +Telemetry: + +- `DISABLE_TELEMETRY=1` - Disable anonymous telemetry (opt-out) +- `UNITY_MCP_DISABLE_TELEMETRY=1` - Same as `DISABLE_TELEMETRY` +- `MCP_DISABLE_TELEMETRY=1` - Same as `DISABLE_TELEMETRY` +- `UNITY_MCP_TELEMETRY_ENDPOINT` - Override telemetry endpoint URL +- `UNITY_MCP_TELEMETRY_TIMEOUT` - Override telemetry request timeout (seconds) + +### Examples + +**Stdio (default):** + +```bash +uvx --from mcpforunityserver mcp-for-unity --transport stdio +``` + +**HTTP (local):** + +```bash +uvx --from mcpforunityserver mcp-for-unity --transport http --http-host 127.0.0.1 --http-port 8080 +``` + +**HTTP (remote-hosted):** + +```bash +uvx --from mcpforunityserver mcp-for-unity --transport http --http-host 0.0.0.0 --http-port 8080 --http-remote-hosted +``` + +**Disable telemetry:** + +```bash +DISABLE_TELEMETRY=1 uvx --from mcpforunityserver mcp-for-unity --transport stdio +``` --- From 45f8cf1e9198f32d451a772f437d9beb85dcde60 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Wed, 28 Jan 2026 18:01:44 -0400 Subject: [PATCH 05/50] feat: add API key authentication support for remote-hosted HTTP transport - Add API key field to connection UI (visible only in HTTP Remote mode) - Add "Get API Key" and "Clear" buttons with login URL retrieval - Include X-API-Key header in WebSocket connections when configured - Add API key to CLI commands (mcp add, claude mcp add) when set - Update config.json generation to include headers with API key - Add API key validation service with caching and configurable endpoints - Add /api/auth/login-url endpoint --- .../Clients/McpClientConfiguratorBase.cs | 26 +- .../Editor/Constants/EditorPrefKeys.cs | 2 + .../Editor/Helpers/ConfigJsonBuilder.cs | 15 +- .../Transports/WebSocketTransportClient.cs | 8 + .../Connection/McpConnectionSection.cs | 169 ++++++++++++- .../Connection/McpConnectionSection.uxml | 10 + Server/src/core/config.py | 10 + Server/src/main.py | 139 ++++++++++- Server/src/services/api_key_service.py | 235 ++++++++++++++++++ Server/src/services/resources/editor_state.py | 9 +- .../src/services/resources/unity_instances.py | 6 +- .../src/services/tools/set_active_instance.py | 6 +- Server/src/transport/plugin_hub.py | 84 ++++++- Server/src/transport/plugin_registry.py | 87 +++++-- .../transport/unity_instance_middleware.py | 36 ++- Server/src/transport/unity_transport.py | 29 ++- 16 files changed, 813 insertions(+), 58 deletions(-) create mode 100644 Server/src/services/api_key_service.py diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 00e440cc1..de163e7f2 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -604,7 +604,16 @@ private void RegisterWithCapturedValues( string args; if (useHttpTransport) { - args = $"mcp add --transport http UnityMCP {httpUrl}"; + // Add API key header if configured (for remote-hosted mode) + string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); + if (!string.IsNullOrEmpty(apiKey)) + { + args = $"mcp add --transport http UnityMCP {httpUrl} --header \"X-API-Key: {apiKey}\""; + } + else + { + args = $"mcp add --transport http UnityMCP {httpUrl}"; + } } else { @@ -664,7 +673,16 @@ private void Register() if (useHttpTransport) { string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - args = $"mcp add --transport http UnityMCP {httpUrl}"; + // Add API key header if configured (for remote-hosted mode) + string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); + if (!string.IsNullOrEmpty(apiKey)) + { + args = $"mcp add --transport http UnityMCP {httpUrl} --header \"X-API-Key: {apiKey}\""; + } + else + { + args = $"mcp add --transport http UnityMCP {httpUrl}"; + } } else { @@ -757,8 +775,10 @@ public override string GetManualSnippet() if (useHttpTransport) { string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); + string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); + string headerArg = !string.IsNullOrEmpty(apiKey) ? $" --header \"X-API-Key: {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" + diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index a6e81b4ac..d6f7c7422 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -54,5 +54,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 dd00d73d0..895e2b399 100644 --- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Clients.Configurators; +using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using Newtonsoft.Json; @@ -73,6 +73,19 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl if (unity["command"] != null) unity.Remove("command"); if (unity["args"] != null) unity.Remove("args"); + // Add API key header if configured (for remote-hosted mode) + string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); + if (!string.IsNullOrEmpty(apiKey)) + { + var headers = new JObject { ["X-API-Key"] = apiKey }; + unity["headers"] = headers; + } + else + { + // Remove headers if API key is not set + if (unity["headers"] != null) unity.Remove("headers"); + } + if (isVSCode) { unity["type"] = "http"; diff --git a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs index 65b4e4873..c76180bf6 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; @@ -198,6 +199,13 @@ private async Task EstablishConnectionAsync(CancellationToken token) _socket = new ClientWebSocket(); _socket.Options.KeepAliveInterval = _socketKeepAliveInterval; + // Add API key header if configured (for remote-hosted mode) + string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); + if (!string.IsNullOrEmpty(apiKey)) + { + _socket.Options.SetRequestHeader("X-API-Key", apiKey); + } + try { await _socket.ConnectAsync(_endpointUri, connectionToken).ConfigureAwait(false); diff --git a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs index 6a2586665..886cd3181 100644 --- a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs @@ -46,6 +46,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; @@ -95,6 +102,12 @@ private void CacheUIElements() statusIndicator = Root.Q("status-indicator"); connectionStatusLabel = Root.Q