Skip to content

Conversation

@msanatan
Copy link
Member

@msanatan msanatan commented Jan 29, 2026

Description

The MCP HTTP server can be used remotely, but it's not really fit for use remotely.

For e.g. an organization may host this remotely for their company, but anyone with the URL can interact with their server.

Even worse, the fallbacks around mapping Unity sessions with MCP clients sessions can give a user access to a project they should never see. Even the tool to list instances, it shows everyone connected and in a multi-tenant environment that's undesirable.

This change secures remote deployments with a flexible API Key system. MCP allows for Oauth2, and FastMCP supports it as well. But I think approach is better for this server.

In our case, MCP for Unity does not control Unity identities, or any other identities. We can run our own identity provider and take advantage of FastMCP's OAuth feature, but that's out of scope for this server's purpose.

This models allows any auth service to integrate. The external auth services need 2 things:

  • A way to generate API keys
  • An endpoint that validates API keys

With just those 2 things, we can fully isolate sessions in the MCP server, so on a remotely hosted machine a user only interacts with their projects. It also stops an unauthorized user from doing invasive things like listing all active instances.

Type of Change

  • New feature (non-breaking change that adds functionality)

Changes Made

  • Plugin's HTTP remote section has input for an API key
  • When API keys are set, MCP client configs send it as a header
  • MCP server can now be configured to be in HTTP remote mode, disable CLI commands, fallbacks for assigning sessions, and mandating the use of API keys

Testing/Screenshots/Recordings

Screenshot 2026-01-28 at 9 29 25 PM

Documentation Updates

  • I have added/removed/modified tools or resources
  • If yes, I have updated all documentation files using:
    • The LLM prompt at tools/UPDATE_DOCS_PROMPT.md (recommended)
    • Manual updates following the guide at tools/UPDATE_DOCS.md

I've added documentation for server users and for the dev team.

Related Issues

Closes #433

Additional Notes

Summary by Sourcery

Add API key–based authentication and remote-hosted mode for the MCP for Unity HTTP server, isolating Unity sessions per user and tightening exposed HTTP/CLI surfaces for remote deployments.

New Features:

  • Introduce configurable remote-hosted HTTP mode that requires explicit Unity instance selection and supports external API key validation.
  • Add API key management UI and storage in the Unity editor connection window, propagating API keys to MCP client registrations and WebSocket connections.
  • Expose an /api/auth/login-url endpoint so clients can discover where to obtain or manage API keys.

Enhancements:

  • Scope Unity plugin sessions by user in the registry and routing logic to prevent cross-tenant access in multi-user setups.
  • Refine Unity instance listing, selection, and auto-selection behavior to respect user isolation and remote-hosted constraints.
  • Improve CLI instance listing error handling and telemetry test reliability.
  • Document the remote server authentication architecture and configuration for operators and contributors.

Tests:

  • Add comprehensive integration and unit tests for API key validation, WebSocket auth gating, user-scoped session isolation, middleware behavior, and startup auth configuration validation.

Summary by CodeRabbit

  • New Features

    • Remote-hosted API key auth with server-side validation, per-user session isolation, and distinct local vs remote HTTP transports; Editor UI for entering/getting/clearing API keys and client-side header injection.
  • Documentation

    • New guides, architecture reference, CLI/env docs, examples, and Docker notes describing remote-hosted auth and usage.
  • Tests

    • Extensive integration tests covering API key validation, WebSocket/HTTP auth flows, multi-user session isolation, routing, and startup validation.

✏️ Tip: You can customize this high-level summary in your review settings.

The newer /api/instances covers that data, and we want to remove these "expose all" endpoints
…port

- 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
- Add UNITY_MCP_HTTP_REMOTE_HOSTED environment variable as alternative to --http-remote-hosted flag
- Accept "true", "1", or "yes" values (case-insensitive)
- Update CLI help text to document environment variable option
…isting

- Raise ValueError when list_sessions() called without user_id in remote-hosted mode
- Add comprehensive integration tests for multi-user session isolation
- Add unit tests for PluginRegistry user-scoped session filtering
- Verify cross-user isolation with same project hash
- Test unity_instances resource and set_active_instance user filtering
- Add ApiKeyService tests covering validation, caching, retries, and singleton lifecycle
- Add startup config validation tests for remote-hosted mode requirements
- Test cache hit/miss scenarios, TTL expiration, and manual invalidation
- Test transient failure handling (5xx, timeouts, connection errors) with retry logic
- Test service token header injection and empty key fast-path validation
- Test startup validation requiring
…ation tests

Ensures test isolation for config-dependent integration tests
Prevents unnecessary API key validation when not in remote-hosted mode
- Add client_id to test context mock in set_active_instance test
- Add get_state mock to context in global instance routing test
- Add user guide covering configuration, setup, and troubleshooting
- Add architecture reference documenting internal design and request flows
@msanatan msanatan self-assigned this Jan 29, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

Adds API-key authentication and remote-hosted transport scope: editor UI/prefs and client header injection (HTTP/WebSocket); threads apiKey and serverTransport through CLI configurator and registration flows; server-side ApiKeyService for external validation and caching; enforces per-user session isolation and user-scoped registry/middleware.

Changes

Cohort / File(s) Summary
Editor configurator & snippets
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Thread apiKey and serverTransport through Configure/Register; include X-API-Key header in remote HTTP registration and manual CLI snippets; add SanitizeShellHeaderValue.
Editor prefs & constants
MCPForUnity/Editor/Constants/EditorPrefKeys.cs, MCPForUnity/Editor/Constants/AuthConstants.cs
Add EditorPrefs keys for remote base URL and ApiKey; add X-API-Key constant.
Editor config builder
MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
Inject/remove X-API-Key header into unityMCP HTTP config when remote-hosted and apiKey present.
Editor UI & client transport
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs, .../McpConnectionSection.uxml, MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs, MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
Add API Key UI (field, Get/Clear buttons, login-url fetch/cache), persist to EditorPrefs, show only in HTTP-remote mode; WebSocket client sends X-API-Key; UI passes apiKey and serverTransport to configurator.
Endpoint utilities & transport enum
MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs, MCPForUnity/Editor/Models/McpStatus.cs, MCPForUnity/Editor/Services/EditorConfigurationCache.cs
Introduce local vs remote base URLs, scope helpers, GetCurrentServerTransport(), and new HttpRemote transport; persist remote base URL in EditorConfigurationCache.
Local server management
MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs, MCPForUnity/Editor/Services/ServerManagementService.cs
Switch local URL lookups to GetLocalBaseUrl() and local-focused flows for start/stop, PID, and command building.
Server protocol & constants
Server/src/core/constants.py
Add server-side API_KEY_HEADER = "X-API-Key".
Server config & startup
Server/src/core/config.py, Server/src/main.py
Add http_remote_hosted and API-key config (validation/login URL, TTL, service token/header); require validation URL when remote-hosted; add /api/auth/login-url; initialize ApiKeyService.
ApiKey validation service
Server/src/services/api_key_service.py
New ApiKeyService with external validation POST, retries, TTL cache, redaction, and validate/invalidate/clear APIs; ValidationResult dataclass.
WebSocket auth, hub & registry
Server/src/transport/plugin_hub.py, Server/src/transport/plugin_registry.py
Validate X-API-Key on WS connect in remote-hosted mode, store user_id/metadata on ws.state, thread user_id into register/command flows; add user-scoped session mappings and InstanceSelectionRequiredError.
HTTP transport & middleware
Server/src/transport/unity_transport.py, Server/src/transport/unity_instance_middleware.py
Resolve user_id from API key for HTTP requests, require auth when remote-hosted, propagate user_id into send/resolve flows and inject into request context.
Session/resources/tools flows
Server/src/services/resources/editor_state.py, Server/src/services/resources/unity_instances.py, Server/src/services/tools/set_active_instance.py
When http_remote_hosted, filter sessions by user_id and pass user_id to session resolution / set-active flows.
CLI & instance listing
Server/src/cli/utils/connection.py
Simplify instance listing to /api/instances and add explicit HTTP error handling.
Tests
Server/tests/...
Add extensive tests covering ApiKeyService, startup validation, middleware/WS auth, multi-user isolation, registry isolation, resolve_user_id, and update/remove legacy session tests.
Docs & deployment
Server/README.md, docs/guides/REMOTE_SERVER_AUTH.md, docs/reference/REMOTE_SERVER_AUTH_ARCHITECTURE.md, Server/DOCKER_OVERVIEW.md
New/expanded docs for remote-hosted mode, API-key auth, CLI/env flags, architecture, examples, and deployment notes.

Sequence Diagram(s)

sequenceDiagram
    actor UnityEditor
    participant MCPClient as MCP Client
    participant MCPHTTP as MCP HTTP Server
    participant MCPWS as MCP WebSocket Hub
    participant ApiKeySvc as ApiKeyService
    participant ExtAuth as External Auth Service
    participant Registry as PluginRegistry/PluginHub

    UnityEditor->>MCPClient: store X-API-Key in EditorPrefs
    MCPClient->>MCPHTTP: HTTP request with X-API-Key
    MCPHTTP->>ApiKeySvc: validate(api_key)
    alt cache hit
        ApiKeySvc-->>MCPHTTP: ValidationResult(user_id)
    else cache miss
        ApiKeySvc->>ExtAuth: POST validate_url (api_key, service token)
        ExtAuth-->>ApiKeySvc: valid,user_id,metadata
        ApiKeySvc-->>MCPHTTP: ValidationResult(user_id)
    end
    MCPHTTP->>Registry: get_sessions(user_id)
    Registry-->>MCPHTTP: user-scoped sessions

    MCPClient->>MCPWS: WS upgrade with X-API-Key
    MCPWS->>ApiKeySvc: validate(api_key)
    alt valid
        ApiKeySvc-->>MCPWS: ValidationResult(user_id)
        MCPWS->>MCPWS: store ws.state.user_id (rgba(0,128,0,0.5))
        MCPClient->>MCPWS: register(project_hash)
        MCPWS->>Registry: register(user_id, project_hash)
        Registry-->>MCPWS: PluginSession (user-scoped)
    else invalid
        MCPWS-->>MCPClient: close (auth failure)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 I found a tiny secret key,

I tucked it in prefs so snug and deep.
Servers ask and keys reply,
Sessions nest for you and I.
Hop secure — now code can sleep.

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Remote server auth' is vague and generic—it lacks specificity about the primary feature (API key authentication for remote-hosted mode with session isolation). Use a more descriptive title such as 'Add API key–based authentication and remote-hosted mode for MCP HTTP server' or 'Enable API key auth and user isolation for remote MCP deployments'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description comprehensively addresses the required sections: problem statement, type of change (new feature), detailed changes made, related issue link, and documentation updates noted. All expected content is present and complete.
Linked Issues check ✅ Passed The PR fully implements the primary objectives from issue #433: API key-based authentication, remote-hosted HTTP mode with user session isolation, ingress enforcement, CLI/env configuration, Unity UI for API key management, and comprehensive test coverage for validation and session isolation.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #433 requirements: API key auth, remote-hosted mode, session isolation, UI updates, server config, and supporting tests. No extraneous refactoring or unrelated feature creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 29, 2026

Reviewer's Guide

Adds API key–based authentication and multi-tenant isolation for the remote HTTP server, wiring an external validation service into WebSocket and HTTP flows, scoping Unity plugin sessions per user, exposing a login-url endpoint, and updating Unity editor UI, MCP client config generation, and docs/tests accordingly.

Sequence diagram for authenticated MCP HTTP tool call with API key

sequenceDiagram
    participant Client as MCPClient
    participant Server as McpForUnityServer
    participant Middleware as UnityInstanceMiddleware
    participant Transport as UnityTransport
    participant ApiKeySvc as ApiKeyService
    participant Registry as PluginRegistry
    participant Hub as PluginHub
    participant Unity as UnityEditorPlugin

    Client->>Server: HTTP POST /mcp
    activate Server
    Server->>Middleware: on_call_tool
    activate Middleware
    Middleware->>Transport: _resolve_user_id_from_request
    activate Transport
    Transport->>ApiKeySvc: validate(api_key)
    activate ApiKeySvc
    ApiKeySvc-->>Transport: ValidationResult(valid true user_id)
    deactivate ApiKeySvc
    Transport-->>Middleware: user_id
    deactivate Transport
    Middleware->>Middleware: ctx.set_state(user_id)
    Middleware->>Middleware: get_active_instance or _maybe_autoselect_instance
    Middleware->>Middleware: ctx.set_state(unity_instance)
    Middleware-->>Server: call_next()
    deactivate Middleware

    Server->>Transport: send_with_unity_instance(command_type params unity_instance)
    activate Transport
    Transport->>Hub: send_command_for_instance(unity_instance command_type params user_id)
    activate Hub
    Hub->>Registry: _resolve_session_id(unity_instance user_id)
    activate Registry
    Registry-->>Hub: session_id
    deactivate Registry
    Hub->>Unity: send_command(session_id command_type params)
    Unity-->>Hub: result
    Hub-->>Transport: raw_result
    deactivate Hub
    Transport-->>Server: normalize_unity_response(raw_result)
    deactivate Transport

    Server-->>Client: MCP tool result
    deactivate Server
Loading

Sequence diagram for Unity WebSocket plugin authentication and registration

sequenceDiagram
    participant Plugin as UnityEditorPlugin
    participant Hub as PluginHub
    participant ApiKeySvc as ApiKeyService
    participant Registry as PluginRegistry

    Plugin->>Hub: WS connect /hub/plugin with header X-API-Key
    activate Hub
    Hub->>ApiKeySvc: validate(api_key)
    activate ApiKeySvc
    ApiKeySvc-->>Hub: ValidationResult(valid true user_id)
    deactivate ApiKeySvc
    Hub->>Hub: websocket.state.user_id = user_id
    Hub-->>Plugin: accept()

    Plugin->>Hub: RegisterMessage(project_name project_hash unity_version)
    Hub->>Registry: register(session_id project_name project_hash unity_version user_id)
    activate Registry
    Registry->>Registry: update _sessions
    Registry->>Registry: update _user_hash_to_session[(user_id project_hash)]
    Registry-->>Hub: PluginSession
    deactivate Registry
    Hub-->>Plugin: RegisteredMessage(session_id)
    deactivate Hub
Loading

Class diagram for API key auth and session isolation components

classDiagram
    class ApiKeyService {
        -str _validation_url
        -float _cache_ttl
        -str _service_token_header
        -str _service_token
        -dict~str tuple~bool str dict float~~ _cache
        -asyncio.Lock _cache_lock
        +REQUEST_TIMEOUT: float
        +MAX_RETRIES: int
        +ApiKeyService(validation_url: str, cache_ttl: float, service_token_header: str, service_token: str)
        +validate(api_key: str) ValidationResult
        +invalidate_cache(api_key: str) void
        +clear_cache() void
        -_validate_external(api_key: str) ValidationResult
        +get_instance() ApiKeyService
        +is_initialized() bool
    }

    class ValidationResult {
        +bool valid
        +str user_id
        +dict~str any~ metadata
        +str error
        +bool cacheable
    }

    class ServerConfig {
        +bool http_remote_hosted
        +str api_key_validation_url
        +str api_key_login_url
        +float api_key_cache_ttl
        +str api_key_service_token_header
        +str api_key_service_token
    }

    class PluginSession {
        +str session_id
        +str project
        +str project_hash
        +str unity_version
        +datetime registered_at
        +datetime connected_at
        +dict~str ToolDefinitionModel~ tools
        +str project_id
        +str user_id
    }

    class PluginRegistry {
        -dict~str PluginSession~ _sessions
        -dict~str str~ _hash_to_session
        -dict~tuple~str str~ str~ _user_hash_to_session
        -asyncio.Lock _lock
        +register(session_id: str, project_name: str, project_hash: str, unity_version: str, user_id: str) PluginSession
        +unregister(session_id: str) void
        +get_session(session_id: str) PluginSession
        +get_session_id_by_hash(project_hash: str) str
        +get_session_id_by_user_hash(user_id: str, project_hash: str) str
        +list_sessions(user_id: str) dict~str PluginSession~
        +register_tools_for_session(session_id: str, tools: list~ToolDefinitionModel~) void
    }

    class PluginHub {
        <<singleton>>
        -dict~str WebSocket~ _connections
        +on_connect(websocket: WebSocket) void
        +get_sessions(user_id: str) SessionList
        +send_command_for_instance(unity_instance: str, command_type: str, params: dict~str any~, user_id: str) dict~str any~
        +_resolve_session_id(unity_instance: str, user_id: str) str
    }

    class UnityInstanceMiddleware {
        +get_session_key(ctx: Context) str
        +on_call_tool(context: MiddlewareContext, call_next: Callable) any
        +on_read_resource(context: MiddlewareContext, call_next: Callable) any
        -_inject_unity_instance(context: MiddlewareContext) void
        -_maybe_autoselect_instance(ctx: Context) str
        -_resolve_user_id() str
    }

    class UnityTransport {
        +send_with_unity_instance(ctx: Context, send_fn: Callable, unity_instance: str, command_type: str, params: dict~str any~, user_id: str) any
        +_resolve_user_id_from_request() str
    }

    ApiKeyService --> ValidationResult
    PluginHub --> PluginRegistry
    PluginRegistry --> PluginSession
    UnityInstanceMiddleware --> PluginHub
    UnityInstanceMiddleware --> UnityTransport
    UnityInstanceMiddleware --> ServerConfig
    UnityTransport --> ApiKeyService
    UnityTransport --> ServerConfig
    ApiKeyService --> ServerConfig
Loading

File-Level Changes

Change Details Files
Introduce centralized API key validation service and wire it into remote-hosted HTTP/WebSocket flows.
  • Add ApiKeyService singleton with external HTTP validation, caching, retries, and service-token support.
  • Validate X-API-Key on plugin WebSocket connect, rejecting unauthorized connections and storing user_id/metadata in connection state.
  • Resolve user_id from HTTP request headers for tool/resource calls, failing closed when missing in remote-hosted mode.
Server/src/services/api_key_service.py
Server/src/transport/plugin_hub.py
Server/src/transport/unity_transport.py
Server/src/transport/unity_instance_middleware.py
Server/tests/integration/test_api_key_service.py
Server/tests/integration/test_plugin_hub_websocket_auth.py
Server/tests/integration/test_resolve_user_id.py
Server/tests/integration/test_middleware_auth_integration.py
Scope Unity plugin sessions per user and enforce isolation in remote-hosted mode.
  • Extend PluginRegistry to track user_id on sessions and maintain both hash and (user_id, hash) indexes.
  • Update session listing and resolution APIs to accept user_id and guard against cross-user leaks in remote-hosted mode.
  • Propagate user_id through PluginHub session APIs and all HTTP-side callers (tools/resources, editor_state inference, unity_instances, set_active_instance).
Server/src/transport/plugin_registry.py
Server/src/transport/plugin_hub.py
Server/src/services/resources/editor_state.py
Server/src/services/resources/unity_instances.py
Server/src/services/tools/set_active_instance.py
Server/tests/integration/test_plugin_registry_user_isolation.py
Server/tests/integration/test_multi_user_session_isolation.py
Server/tests/integration/test_instance_routing_comprehensive.py
Add remote-hosted configuration flags, API-key-related CLI/env options, and auth/login helper HTTP endpoints.
  • Extend ServerConfig with http_remote_hosted and API-key-related fields (validation URL, login URL, cache TTL, service token).
  • Add CLI args and env parsing for remote-hosted and API-key options, with startup validation that remote-hosted requires a validation URL.
  • Register /api/auth/login-url route and conditionally disable CLI-only HTTP routes in remote-hosted mode.
Server/src/core/config.py
Server/src/main.py
Server/tests/integration/test_auth_config_startup.py
Update Unity editor plugin UI and transports to support API-key entry and propagation to MCP clients and WebSocket connections.
  • Add API key row/controls in the connection window for HTTP Remote, persisting the key in EditorPrefs and providing Get/Clear actions via /api/auth/login-url.
  • Send X-API-Key header from WebSocketTransportClient when configured.
  • Inject X-API-Key header into generated MCP client configs (JSON and CLI snippets) when an API key is present.
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.uxml
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Tighten behavior of instance selection and CLI utilities under remote-hosted mode and clean up related tests/docs.
  • Require explicit Unity instance selection in remote-hosted HTTP flows, removing auto-selection when multiple sessions exist and adding clearer error messages.
  • Simplify CLI instance listing to use /api/instances only, with better error messages, and disable that endpoint in remote-hosted mode.
  • Document remote-server auth architecture and operational guide, and update server README with new CLI/env options and examples; adjust telemetry and other integration tests accordingly.
Server/src/transport/plugin_hub.py
Server/src/transport/unity_instance_middleware.py
Server/src/cli/utils/connection.py
Server/tests/test_cli.py
Server/tests/integration/test_telemetry_queue_worker.py
Server/README.md
docs/reference/REMOTE_SERVER_AUTH_ARCHITECTURE.md
docs/guides/REMOTE_SERVER_AUTH.md

Assessment against linked issues

Issue Objective Addressed Explanation
#433 Implement an optional authentication system for the MCP HTTP/WebSocket server that preserves the current no-auth default, and when enabled enforces both (a) a configurable IP allowlist (supporting *, single IP, and CIDR) and (b) a simple bearer token (auth_token) on all HTTP routes, WebSocket upgrades, and tool/resource execution, with configuration via auth_enabled, allowed_ips, and auth_token from env/CLI. The PR introduces an API key–based auth model for a special http_remote_hosted mode using an external validation service and X-API-Key headers, but it does not implement any IP allowlist, does not define or use auth_enabled, allowed_ips, or auth_token, and does not use a simple bearer token checked directly by the server. Enforcement is tied to remote-hosted mode and external validation rather than the requested built-in IP filter plus bearer token configuration.
#433 Add Unity UI and client behavior changes for authentication: a new Auth section in the Unity UI to configure allowlist and token, and a server-to-client signal indicating when auth is required so MCP clients can prompt for and send Authorization: Bearer <token>. The PR adds an API key field and related controls in the Unity connection UI, and MCP clients are configured to send an X-API-Key header, but there is no UI for configuring an IP allowlist or server-side auth token, and there is no protocol-level signal from the server that auth is required or that a bearer token should be used. The behavior is client-side configuration of an API key, not the requested Auth panel for allowlist + token nor the specified bearer Authorization flow.
#433 Provide observability and documentation for the new auth system: startup logs describing auth state and allowlist (with a warning if auth is enabled but token empty), and README/docs describing how to enable auth, sample configurations (env/CLI), and client usage. The PR adds documentation and internal architecture docs for a remote server API key authentication model and logs when the API key service is initialized, but it does not log or document any IP allowlist configuration or a simple auth_token-based bearer scheme, nor does it cover the specific auth_enabled/allowed_ips/auth_token configuration surface or the warning semantics around an enabled auth mode with an empty token that the issue requests.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, 2 other issues, and left some high level feedback:

Security issues:

  • Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (link)

General comments:

  • The X-API-Key header name is hard‑coded in multiple places (WebSocket transport, HTTP client config, CLI args) – consider centralizing it in a single constant/shared config to avoid drift if it ever needs to change.
  • The repeated error message "Unity instance selection is required. Call set_active_instance with Name@hash from mcpforunity://instances." appears in several branches of _resolve_session_id; factoring this into a small helper (or custom exception) would simplify the control flow and reduce duplication.
  • ApiKeyService caches entries keyed directly by the full API key string; if you expect many distinct keys or long‑lived servers, consider adding a bounded cache or eviction strategy to avoid unbounded memory growth and to reduce the exposure window of in‑memory secrets.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `X-API-Key` header name is hard‑coded in multiple places (WebSocket transport, HTTP client config, CLI args) – consider centralizing it in a single constant/shared config to avoid drift if it ever needs to change.
- The repeated error message "Unity instance selection is required. Call set_active_instance with Name@hash from mcpforunity://instances." appears in several branches of `_resolve_session_id`; factoring this into a small helper (or custom exception) would simplify the control flow and reduce duplication.
- ApiKeyService caches entries keyed directly by the full API key string; if you expect many distinct keys or long‑lived servers, consider adding a bounded cache or eviction strategy to avoid unbounded memory growth and to reduce the exposure window of in‑memory secrets.

## Individual Comments

### Comment 1
<location> `Server/src/main.py:385-394` </location>
<code_context>
+                else:
</code_context>

<issue_to_address>
**issue (bug_risk):** Custom tool execution block is now scoped only to the `no unity_instance` branch, changing behavior and likely breaking targeted instance calls.

With this indentation, `if command_type == "execute_custom_tool":` is only evaluated when *no* `unity_instance` is provided. As a result, `/api/command` calls with `type="execute_custom_tool"` and a specific `unity_instance` will bypass `CustomToolService.execute_tool` and instead fall through to `PluginHub.send_command`, which likely cannot handle this command type. If this change wasn’t intentional, the custom-tool branch should be outdented so it runs after session resolution for both targeted and non-targeted instances, restoring the previous behavior.
</issue_to_address>

### Comment 2
<location> `Server/tests/integration/test_multi_user_session_isolation.py:51-60` </location>
<code_context>
+
+
+class TestBasicValidation:
+    @pytest.mark.asyncio
+    async def test_valid_key(self):
+        svc = _make_service()
</code_context>

<issue_to_address>
**suggestion (testing):** Narrow the expected exception type when resolving another user's session hash

In `test_cannot_resolve_other_users_hash`, `with pytest.raises(Exception):` is too broad and can mask unrelated errors (e.g. typos, unexpected runtime failures). Since `_resolve_session_id` raises a specific exception when no matching session is found, assert that concrete type (e.g. `RuntimeError`) and, if useful, the message, so the test more precisely validates the resolution/authorization behavior.
</issue_to_address>

### Comment 3
<location> `Server/tests/integration/test_api_key_service.py:217` </location>
<code_context>
sk-expiry-key-12345
</code_context>

<issue_to_address>
**security (generic-api-key):** Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

*Source: gitleaks*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@docs/guides/REMOTE_SERVER_AUTH.md`:
- Around line 29-37: The markdown violates MD040/MD060 table/style rules and has
unlabeled code fences and a wording nit: update all tables (including the one
listing CLI args and the other tables around the noted sections) to use
consistent pipe/spacing style to satisfy MD060/MD040, add the language
identifier "http" to all request/headers code fences (e.g., the POST validation
block and the service-token header block) so markdownlint recognizes them, and
replace the phrase "fail closed" with a clearer alternative (e.g., "deny by
default" or "reject by default") wherever it appears (the phrase flagged in the
review). Ensure these same fixes are applied consistently to the other affected
table ranges mentioned in the comment.

In `@MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs`:
- Around line 607-616: RegisterWithCapturedValues currently reads
EditorPrefs.GetString (EditorPrefKeys.ApiKey) inside a method that must be
thread-safe; remove that call and add an apiKey parameter to
RegisterWithCapturedValues and to ConfigureWithCapturedValues so the API key is
captured on the main thread alongside httpUrl/uvxPath etc. Update the callers
(including ConfigureWithCapturedValues) to fetch
EditorPrefs.GetString(EditorPrefKeys.ApiKey) on the main thread and pass the
string down; inside RegisterWithCapturedValues use the passed apiKey when
composing args (instead of calling EditorPrefs) so the method remains
thread-safe.

In
`@MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs`:
- Around line 202-207: EstablishConnectionAsync currently calls
EditorPrefs.GetString(EditorPrefKeys.ApiKey, ...) from a background task
(AttemptReconnectAsync/Task.Run), which is not main-thread safe; instead capture
the API key on the main thread in StartAsync (alongside the other identity
values) into a private field (e.g., _apiKey) and then in
EstablishConnectionAsync use that field when calling
_socket.Options.SetRequestHeader("X-API-Key", ...) rather than calling
EditorPrefs.GetString directly; ensure the field is populated before any
reconnect/attempt logic runs so background threads only read the cached string.

In `@MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs`:
- Around line 842-878: The cachedLoginUrl is not invalidated when the HTTP base
URL changes, causing stale redirects; update the logic so cachedLoginUrl is
cleared or made URL-specific whenever the base URL is updated. Specifically,
either clear cachedLoginUrl inside the method that persists/updates the HTTP URL
(e.g., PersistHttpUrlFromField) after calling HttpEndpointUtility.GetBaseUrl(),
or change GetLoginUrlAsync to store/compare the base URL alongside
cachedLoginUrl (use HttpEndpointUtility.GetBaseUrl() as the key) before
returning the cached value; ensure references to cachedLoginUrl and
GetLoginUrlAsync reflect the new invalidation or key check.

In `@Server/src/main.py`:
- Around line 722-725: The env-parsing for UNITY_MCP_HTTP_REMOTE_HOSTED in the
config assignment (affecting config.http_remote_hosted and reading
args.http_remote_hosted) omits the "on" variant; update the os.environ check to
include "on" alongside "true", "1", and "yes" so that values like "on" enable
remote-hosted mode consistently with other toggles.

In `@Server/src/transport/plugin_hub.py`:
- Around line 80-108: on_connect currently allows connections when ApiKeyService
is not initialized and treats any valid result without user_id as acceptable;
update on_connect to fail closed: if ApiKeyService.is_initialized() is false,
reject the websocket (close with a retryable or auth-required code), and after
calling ApiKeyService.get_instance().validate(api_key) ensure both result.valid
and result.user_id are present before accepting; treat transient auth failures
as retryable by checking result.error for broader conditions (e.g.,
"unavailable", timeouts, 5xx or similar indicators) and close with code 1013 so
clients can retry; only when result.valid and result.user_id exist store
websocket.state.user_id and websocket.state.api_key_metadata.

In `@Server/src/transport/unity_transport.py`:
- Around line 11-12: The API key header lookup is case-sensitive because
get_http_headers() returns a plain dict and the code in unity_transport.py
checks headers.get("x-api-key"); change the logic to normalize header keys to
lowercase before lookup (or perform a lowercase lookup using headers with a
comprehension) so variants like "X-API-Key" are matched reliably; update the
code path around the header extraction in the UnityTransport request handling
(the block that calls get_http_headers() and uses headers.get("x-api-key")) to
use the normalized keys and then pass the value to ApiKeyService validation.

In `@Server/tests/integration/test_api_key_service.py`:
- Around line 212-220: Replace secret-like test literals with clearly non-secret
placeholders to avoid secret-scanner false positives: change occurrences of
"sk-expiry-key-12345" used in svc.validate and in the manual cache-manipulation
block (accessing svc._cache and svc._cache_lock) to a benign test string such as
"test-expiry-key-12345" (or another "test-..." name), and similarly update other
flagged literals (e.g. the "svc-secret-..." patterns at the other mentioned
location) to non-secret placeholders throughout the file so tests retain
semantics but no longer resemble real secrets.

In `@Server/tests/integration/test_middleware_auth_integration.py`:
- Around line 70-84: The test method test_get_session_key_uses_user_id_fallback
in class TestMiddlewareSessionKey declares an unused monkeypatch parameter
causing Ruff ARG002; remove the monkeypatch parameter from the method signature
so it becomes def test_get_session_key_uses_user_id_fallback(self): and run
tests to confirm no other references to monkeypatch in that test need updating.

In `@Server/tests/integration/test_multi_user_session_isolation.py`:
- Around line 95-103: Replace the broad exception assertion in
test_cannot_resolve_other_users_hash: import NoUnitySessionError from
transport.plugin_hub alongside PluginHub, and change the context manager to use
pytest.raises(NoUnitySessionError) when calling
PluginHub._resolve_session_id("hashB1", user_id="userA") so the test asserts the
specific NoUnitySessionError instead of Exception.

In `@Server/tests/integration/test_telemetry_queue_worker.py`:
- Around line 11-16: The test attaches caplog.handler to the telemetry logger
(tel_logger = logging.getLogger("unity-mcp-telemetry")) and sets caplog level,
but never guarantees removal of caplog.handler; wrap the setup and test body in
a try/finally (or convert to a fixture) and in the finally call
tel_logger.removeHandler(caplog.handler) (optionally check if caplog.handler in
tel_logger.handlers) to ensure the handler is always removed and avoid leaking
across tests; keep the caplog.set_level("DEBUG", logger="unity-mcp-telemetry")
setup inside the try so teardown always runs.
🧹 Nitpick comments (6)
Server/tests/integration/test_telemetry_queue_worker.py (1)

27-29: Replace fixed sleep with a condition-based wait to reduce flakiness.
A hard-coded 0.2s delay can still be flaky under slow CI or faster locally. Consider polling for the worker to switch queues (with a bounded timeout) so the test is deterministic.

MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

676-685: Consider extracting shared API key header logic.

This API key handling block is duplicated from RegisterWithCapturedValues (lines 607-616). Once the thread-safety issue above is fixed by passing the API key as a parameter, consider extracting a helper method to reduce duplication.

Server/src/cli/utils/connection.py (1)

164-180: Consider adding exception chaining for better debugging.

The exception handling is thorough, but re-raising without from e loses the original traceback, which can make debugging harder. This is a minor improvement for maintainability.

♻️ Proposed fix to preserve exception chain
     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}"
-        )
+        ) from e
     except httpx.TimeoutException:
         raise UnityConnectionError(
             "Connection to Unity timed out while listing instances. "
             "Unity may be busy or unresponsive."
-        )
+        ) from None
     except httpx.HTTPStatusError as e:
         raise UnityConnectionError(
             f"HTTP error from server: {e.response.status_code} - {e.response.text}"
-        )
+        ) from e
     except Exception as e:
-        raise UnityConnectionError(f"Unexpected error: {e}")
+        raise UnityConnectionError(f"Unexpected error: {e}") from e
docs/reference/REMOTE_SERVER_AUTH_ARCHITECTURE.md (1)

142-150: Consider adding language hints to fenced code blocks.

Adding language identifiers (e.g., python, text, or plaintext) to fenced code blocks improves syntax highlighting and accessibility in rendered Markdown. The ASCII diagrams at the top are fine without language hints, but the pseudocode sections would benefit.

📝 Example fix for one block
-```
+```text
 _resolve_user_id_from_request()
   -> if not config.http_remote_hosted: return None
   ...

Also applies to: 189-212, 300-304

Server/src/transport/unity_instance_middleware.py (1)

12-12: Gate user_id-based session keys to remote-hosted mode.

get_session_key uses user_id whenever it is present; if any local-mode code sets it, the "global" fallback is bypassed and sessions may fragment unexpectedly. Consider checking config.http_remote_hosted before using user_id to match the docstring.

♻️ Suggested adjustment
-        user_id = ctx.get_state("user_id")
-        if isinstance(user_id, str) and user_id:
-            return f"user:{user_id}"
+        if config.http_remote_hosted:
+            user_id = ctx.get_state("user_id")
+            if isinstance(user_id, str) and user_id:
+                return f"user:{user_id}"

Also applies to: 59-75

Server/tests/integration/test_plugin_registry_user_isolation.py (1)

33-48: Make local-mode assumptions explicit in tests.

These tests call list_sessions() without user_id, which only works when config.http_remote_hosted is false. Pinning the mode avoids brittleness if defaults change; apply similarly to other local-mode tests here.

🔧 Example adjustment
-    async def test_cross_user_isolation_same_hash(self):
+    async def test_cross_user_isolation_same_hash(self, monkeypatch):
+        monkeypatch.setattr(config, "http_remote_hosted", False)
         registry = PluginRegistry()

…sted mode

- Validate that user_id is present after successful key validation
- Expand transient error detection to include timeout and service errors
- Use consistent 1013 status code for retryable auth failures
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New security issues found

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Server/src/transport/unity_transport.py (1)

29-69: Fail closed on missing/invalid API key in remote-hosted HTTP path.
Right now, a missing/invalid API key leads to user_id=None and a generic retry response from the HTTP path. In remote-hosted mode, that should surface as an auth failure so clients can correct credentials instead of retrying.

🔧 Suggested fix (explicit auth failure)
         if user_id is None:
             user_id = await _resolve_user_id_from_request()
+
+        if config.http_remote_hosted and not user_id:
+            return normalize_unity_response(
+                MCPResponse(
+                    success=False,
+                    error="auth_required",
+                    message="API key required",
+                ).model_dump()
+            )
Server/src/transport/plugin_registry.py (1)

49-92: Guard against registering unscoped sessions in remote-hosted mode.
register() currently falls back to local-mode mapping when user_id is falsy. If upstream ever misses user_id in remote-hosted mode, that undermines session isolation. Fail fast when http_remote_hosted is enabled and user_id is missing.

🔧 Suggested fix (fail closed)
     async def register(
         self,
         session_id: str,
         project_name: str,
         project_hash: str,
         unity_version: str,
         project_path: str | None = None,
         user_id: str | None = None,
     ) -> PluginSession:
         """Register (or replace) a plugin session.
@@
         """
+        if config.http_remote_hosted and not user_id:
+            raise ValueError("user_id is required in remote-hosted mode")
 
         async with self._lock:
             now = datetime.now(timezone.utc)
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs`:
- Around line 610-618: The CLI argument string that embeds the API key into args
(the pattern "mcp add --transport http UnityMCP {httpUrl} --header \"X-API-Key:
{apiKey}\"") must shell-escape the apiKey before interpolation to prevent
breaking the command or injection; update the code that constructs args (used in
the async register, sync register, and manual snippet sites) to run the apiKey
through an escaping helper (e.g., escape quotes, backslashes, and problematic
shell chars or percent-encode header values) and use the escaped value in the
header string, or switch to a safe API/parameter array to pass the header
instead of inline string building. Ensure the same escaping helper is applied to
all three call sites where args is built with the X-API-Key header.

In `@MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs`:
- Around line 155-160: The api key is shown in clear text; update the
McpConnectionSection initialization to make the UI field a password field by
setting apiKeyField.isPasswordField = true after you set apiKeyField.value and
tooltip so the input is masked; ensure you still load/save using
EditorPrefKeys.ApiKey and keep existing tooltip behavior when setting
isPasswordField on the apiKeyField VisualElement/TextField instance.

@msanatan
Copy link
Member Author

@dsarno @Scriptwonder - a million tests later (and new Python tests as well), this remote setup is working well. When hosting the server remotely, there needs to be an external auth provider that can validate the user has access to the MCP server. Two key changes here - the sessions are mapped by session_id and user_id, so a user can only interact with their Unity instances (super important!); everything is gated with an API key.

For my own anxiety, I'm going to do some longer tests on local HTTP and stdio and merge this in

P.S. Sourcery has a security notice in the PR, but the API key they're referencing is in my test file, and it literally begins with "test"

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Server/src/transport/plugin_registry.py (1)

64-94: Avoid mapping selection based on user_id presence in local mode.

In local mode, a non-None user_id would route the session into _user_hash_to_session and skip _hash_to_session, breaking get_session_id_by_hash and other local lookups. Consider branching on config.http_remote_hosted instead of user_id so local mode always populates _hash_to_session.

🔧 Suggested fix
-            if user_id:
+            if config.http_remote_hosted:
                 # Remote-hosted mode: use composite key (user_id, project_hash)
                 composite_key = (user_id, project_hash)
                 previous_session_id = self._user_hash_to_session.get(
                     composite_key)
                 if previous_session_id and previous_session_id != session_id:
                     self._sessions.pop(previous_session_id, None)
                 self._user_hash_to_session[composite_key] = session_id
             else:
                 # Local mode: use project_hash only
                 previous_session_id = self._hash_to_session.get(project_hash)
                 if previous_session_id and previous_session_id != session_id:
                     self._sessions.pop(previous_session_id, None)
                 self._hash_to_session[project_hash] = session_id
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs`:
- Around line 155-162: The apiKeyField block uses a non-existent property
passwordCharacter which will not compile; update the TextField usage in
McpConnectionSection (the apiKeyField initialization) to remove
passwordCharacter and instead set apiKeyField.isPasswordField = true and
apiKeyField.maskChar = '*', keeping the existing EditorPrefs value and tooltip
assignments.
🧹 Nitpick comments (1)
Server/src/transport/unity_transport.py (1)

42-43: Consider logging suppressed exceptions for observability.

The bare except Exception silently swallows all errors, which could mask bugs during debugging or production incidents. Since this is a best-effort helper, returning None on failure is correct, but logging would help diagnose issues.

♻️ Suggested improvement
+import logging
+
+logger = logging.getLogger(__name__)
+
 async def _resolve_user_id_from_request() -> str | None:
     """Extract user_id from the current HTTP request's API key header."""
     if not config.http_remote_hosted:
         return None
     if not ApiKeyService.is_initialized():
         return None
     try:
         from fastmcp.server.dependencies import get_http_headers
         headers = get_http_headers(include_all=True)
         api_key = headers.get("x-api-key")
         if not api_key:
             return None
         service = ApiKeyService.get_instance()
         result = await service.validate(api_key)
         return result.user_id if result.valid else None
-    except Exception:
+    except Exception as exc:
+        logger.debug("Failed to resolve user_id from request: %s", exc)
         return None

Add SanitizeShellHeaderValue() method to escape special shell characters (", \, `, $, !) in API keys before including them in shell command arguments. Apply sanitization to all three locations where API keys are embedded in shell commands (two in RegisterWithCapturedValues, one in GetManualInstructions).

Also fix deprecated passwordCharacter property (now maskChar) and improve exception logging in _resolve_user_id_from_request
…SelectionRequiredError class

Add InstanceSelectionRequiredError exception class with centralized error messages (_SELECTION_REQUIRED and _MULTIPLE_INSTANCES). Replace 4 duplicate RuntimeError raises with new exception type. Update tests to catch InstanceSelectionRequiredError instead of RuntimeError.
… constant across C# and Python codebases

Add AuthConstants class in C# and API_KEY_HEADER constant in Python to centralize the API key header name definition. Update all 8 locations where "X-API-Key" was hardcoded (4 in C#, 4 in Python) to use the new constants instead.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Server/src/transport/plugin_hub.py`:
- Around line 282-291: The get_sessions method currently allows global session
listing when user_id is None, which can leak cross-tenant data in remote-hosted
mode; update get_sessions (and the other similar methods referenced) to check
the remote-hosted flag (e.g., a class/global config such as "remote_hosted" or
similar) and if remote-hosted is true and user_id is None, immediately fail
closed by raising a specific exception (or returning an empty/unauthorized
SessionList) instead of calling cls._registry.list_sessions; apply the same
guard to the other session-listing/lookup methods (those around lines 453-558)
that call cls._registry.list_sessions or similar to ensure no unscoped registry
access occurs.
🧹 Nitpick comments (3)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (1)

776-792: Normalize trimmed API key back into the field.
Trimming before persist can leave the UI showing extra whitespace; consider syncing the trimmed value so display matches stored state.

♻️ Proposed tweak
 string apiKey = apiKeyField.text?.Trim() ?? string.Empty;
 string existingKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);

 if (apiKey != existingKey)
 {
     EditorPrefs.SetString(EditorPrefKeys.ApiKey, apiKey);
+    apiKeyField.SetValueWithoutNotify(apiKey);
     OnManualConfigUpdateRequested?.Invoke();
     McpLog.Info(string.IsNullOrEmpty(apiKey) ? "API key cleared" : "API key updated");
 }
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

817-846: Consider escaping newline characters in SanitizeShellHeaderValue.

The sanitization method escapes shell metacharacters within double quotes, but doesn't handle newline (\n) or carriage return (\r) characters. If an API key somehow contained these, it could break the shell command or cause unexpected behavior with the HTTP header.

🛡️ Optional hardening to reject control characters
 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;
+            case '\n':
+            case '\r':
+            case '\0':
+                // Skip control characters that could break shell/HTTP parsing
+                break;
             default:
                 sb.Append(c);
                 break;
         }
     }
     return sb.ToString();
 }
Server/src/transport/plugin_registry.py (1)

82-95: Clarify behavior when user_id is provided in local mode.

The branching logic uses if user_id: to decide between remote-hosted and local mappings. However, if someone accidentally provides a user_id in local mode (where config.http_remote_hosted is false), the session would be stored in _user_hash_to_session but list_sessions() without a user_id argument would not find it.

Consider making the branch explicitly depend on config.http_remote_hosted for consistency:

🔧 Optional: explicit mode check
-            if user_id:
+            if config.http_remote_hosted and user_id:
                 # Remote-hosted mode: use composite key (user_id, project_hash)
                 composite_key = (user_id, project_hash)
                 ...
-            else:
+            elif not config.http_remote_hosted:
                 # Local mode: use project_hash only
                 ...
+            else:
+                # This shouldn't happen due to validation at line 65-66
+                raise ValueError("user_id is required in remote-hosted mode")

…user session access

Remove conditional logic that only filtered sessions by user_id in remote-hosted mode. Now all session listings are filtered by user_id regardless of hosting mode, ensuring users can only see and interact with their own sessions.
…tional user_id parameter

Merge get_session_id_by_hash and get_session_id_by_user_hash into a single method that accepts an optional user_id parameter. Update all call sites to use the unified method signature with user_id as the second parameter. Update tests and documentation to reflect the simplified API.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@docs/reference/REMOTE_SERVER_AUTH_ARCHITECTURE.md`:
- Around line 103-107: Table pipe spacing is inconsistent and triggers
markdownlint MD060; normalize each Markdown table (including rows containing
`_hash_to_session` and `_user_hash_to_session`) so pipes align with the header
style by ensuring a single space after and before each cell delimiter,
consistent left/right outer pipes, and the separator row (`|---|---|---|`)
matches column count; apply the same normalization to the other flagged table
blocks (around the ranges you noted) so all tables follow the same pipe/space
alignment.
- Around line 7-65: The fenced diagrams and code blocks in the
REMOTE_SERVER_AUTH_ARCHITECTURE document lack language identifiers (triggering
markdownlint MD040); update each triple-backtick fence around the ASCII diagrams
to use ```text and any actual code snippets to use ```python where
appropriate—specifically adjust the blocks showing
UnityInstanceMiddleware.on_call_tool, _resolve_user_id,
PluginHub.send_command_for_instance, PluginHub.on_connect,
PluginRegistry.register, and the websocket/state examples so each fenced block
includes the correct language tag.
🧹 Nitpick comments (1)
Server/tests/integration/test_plugin_registry_user_isolation.py (1)

9-10: Make local-mode assumptions explicit in this test class.
Several tests assume local mode; consider forcing config.http_remote_hosted = False by default so environment config can’t leak into these tests.

💡 Suggested fixture
 class TestRegistryUserIsolation:
+    `@pytest.fixture`(autouse=True)
+    def _force_local_mode(self, monkeypatch):
+        monkeypatch.setattr(config, "http_remote_hosted", False)
+
     `@pytest.mark.asyncio`
     async def test_register_with_user_id_stores_composite_key(self):

Comment on lines +7 to +65
```
MCP Client MCP Server External Auth
(Cursor, etc.) (Python) Service
| | |
| X-API-Key: abc123 | |
| POST /mcp (tool call) | |
|-------------------------->| |
| | |
| UnityInstanceMiddleware.on_call_tool |
| | |
| _resolve_user_id() |
| | |
| | POST /validate |
| | {"api_key": "abc123"} |
| |------------------------------>|
| | |
| | {"valid":true, |
| | "user_id":"user-42"} |
| |<------------------------------|
| | |
| Cache result (TTL) |
| | |
| ctx.set_state("user_id", "user-42") |
| ctx.set_state("unity_instance", "Proj@hash") |
| | |
| PluginHub.send_command_for_instance |
| (user_id scoped session lookup) |
| | |
| Tool result | |
|<--------------------------| |


Unity Plugin MCP Server External Auth
(C# WebSocket) (Python) Service
| | |
| WS /hub/plugin | |
| X-API-Key: abc123 | |
|-------------------------->| |
| | |
| PluginHub.on_connect |
| | POST /validate |
| |------------------------------>|
| | {"valid":true, ...} |
| |<------------------------------|
| | |
| accept() | |
| websocket.state.user_id = "user-42" |
|<--------------------------| |
| | |
| {"type":"register", ...} | |
|-------------------------->| |
| | |
| PluginRegistry.register( |
| ..., user_id="user-42") |
| _user_hash_to_session[("user-42","hash")] = sid |
| | |
| {"type":"registered"} | |
|<--------------------------| |
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks.
markdownlint MD040 flags these blocks; use text for diagrams and python for code snippets where appropriate.

Also applies to: 142-150, 189-212, 220-225, 230-235, 300-304, 320-351

🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@docs/reference/REMOTE_SERVER_AUTH_ARCHITECTURE.md` around lines 7 - 65, The
fenced diagrams and code blocks in the REMOTE_SERVER_AUTH_ARCHITECTURE document
lack language identifiers (triggering markdownlint MD040); update each
triple-backtick fence around the ASCII diagrams to use ```text and any actual
code snippets to use ```python where appropriate—specifically adjust the blocks
showing UnityInstanceMiddleware.on_call_tool, _resolve_user_id,
PluginHub.send_command_for_instance, PluginHub.on_connect,
PluginRegistry.register, and the websocket/state examples so each fenced block
includes the correct language tag.

Comment on lines +103 to +107
| Index | Key | Used In |
|-------|-----|---------|
| `_hash_to_session` | `project_hash -> session_id` | Local mode |
| `_user_hash_to_session` | `(user_id, project_hash) -> session_id` | Remote-hosted mode |

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize table pipe spacing to satisfy markdownlint.
MD060 flags several tables; align pipes/spaces with the header style.

Also applies to: 259-266, 286-290, 313-317, 356-361

🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

105-105: Table column style
Table pipe does not align with header for style "aligned"

(MD060, table-column-style)


105-105: Table column style
Table pipe does not align with header for style "aligned"

(MD060, table-column-style)


105-105: Table column style
Table pipe does not align with header for style "aligned"

(MD060, table-column-style)


106-106: Table column style
Table pipe does not align with header for style "aligned"

(MD060, table-column-style)


106-106: Table column style
Table pipe does not align with header for style "aligned"

(MD060, table-column-style)


106-106: Table column style
Table pipe does not align with header for style "aligned"

(MD060, table-column-style)

🤖 Prompt for AI Agents
In `@docs/reference/REMOTE_SERVER_AUTH_ARCHITECTURE.md` around lines 103 - 107,
Table pipe spacing is inconsistent and triggers markdownlint MD060; normalize
each Markdown table (including rows containing `_hash_to_session` and
`_user_hash_to_session`) so pipes align with the header style by ensuring a
single space after and before each cell delimiter, consistent left/right outer
pipes, and the separator row (`|---|---|---|`) matches column count; apply the
same normalization to the other flagged table blocks (around the ranges you
noted) so all tables follow the same pipe/space alignment.

Support UNITY_MCP_PROJECT_SCOPED_TOOLS environment variable as alternative to --project-scoped-tools command line flag. Accept "true", "1", "yes", or "on" as truthy values (case-insensitive). Update help text to document the environment variable option.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Server/src/main.py`:
- Around line 758-764: The current check using config.http_remote_hosted alone
can block stdio runs; change the conditional in main.py so the "validation URL
required" error only fires when HTTP transport is selected (e.g., check
config.transport indicates "http") in addition to config.http_remote_hosted and
missing config.api_key_validation_url. Update the if-condition that references
config.http_remote_hosted and config.api_key_validation_url (and the following
logger.error/SystemExit(1) block) to also require config.transport == "http" (or
the equivalent transport enum/value used in your codebase) so non-HTTP
transports are not gated by the API key validation URL.
🧹 Nitpick comments (1)
Server/src/main.py (1)

459-461: Prefer logger.exception(...) to capture stack traces in CLI endpoints.
This keeps diagnostics intact while still returning a safe 500 response.

🔧 Suggested change
-            except Exception as e:
-                logger.error(f"CLI command error: {e}")
-                return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+            except Exception as e:
+                logger.exception("CLI command error")
+                return JSONResponse({"success": False, "error": str(e)}, status_code=500)

-            except Exception as e:
-                return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+            except Exception as e:
+                logger.exception("CLI instances error")
+                return JSONResponse({"success": False, "error": str(e)}, status_code=500)

-            except Exception as e:
-                logger.error(f"CLI custom tools error: {e}")
-                return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+            except Exception as e:
+                logger.exception("CLI custom tools error")
+                return JSONResponse({"success": False, "error": str(e)}, status_code=500)

Also applies to: 478-479, 540-542

…oth http_remote_hosted is enabled AND transport mode is "http", preventing false validation errors in stdio mode.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@Server/src/main.py`:
- Around line 459-461: Replace the logger.error(...) calls inside the exception
handlers that catch "except Exception as e:" (the handler that logs "CLI command
error" and the similar handler around the other JSONResponse return) with
logger.exception(...) so the traceback is preserved; keep the same descriptive
message (e.g., "CLI command error") and still return JSONResponse({"success":
False, "error": str(e)}, status_code=500) in those except blocks so behavior is
unchanged but the full stack trace is logged.
- Around line 406-457: The execute_custom_tool handling is currently nested
inside the "no unity_instance" else branch so calls that include unity_instance
skip custom tool resolution; refactor by pulling the execute_custom_tool block
out so it runs before the final PluginHub.send_command call regardless of
whether unity_instance was supplied — detect command_type ==
"execute_custom_tool", validate params/tool_name/tool_params (as in the current
block), pick session_id/session_details the same way if needed, compute
unity_instance_hint = unity_instance or session_details.hash, call
resolve_project_id_for_unity_instance(unity_instance_hint) and then
CustomToolService.get_instance().execute_tool(project_id, tool_name,
unity_instance_hint, tool_params) and return its result (as JSONResponse) so the
code does not fall through to PluginHub.send_command.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…emote URLs

Split HTTP transport into HttpLocal and HttpRemote modes with separate EditorPrefs storage (HttpBaseUrl and HttpRemoteBaseUrl).

Add HttpEndpointUtility methods to get/save local and remote URLs independently, and introduce IsRemoteScope() and GetCurrentServerTransport() helpers to centralize 3-way transport determination (Stdio/Http/HttpRemote). Update all client configuration code to distinguish between local and remote HTTP
…remote-hosted mode

Update all locations where API key headers are added to HTTP/WebSocket configurations to check HttpEndpointUtility.IsRemoteScope() or serverTransport == HttpRemote before including the API key. This prevents local HTTP mode from unnecessarily including API key headers in shell commands, config JSON, and WebSocket connections.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (1)

294-305: ⚠️ Potential issue | 🟠 Major

Move GetCurrentServerTransport() to the main thread before the async task.
Calling this inside Task.Run risks touching Unity/EditorPrefs APIs off the main thread. Capture it alongside the other main-thread values.

🔧 Proposed fix
-            Task.Run(() =>
-            {
-                try
-                {
-                    if (client is ClaudeCliMcpConfigurator cliConfigurator)
-                    {
-                        var serverTransport = HttpEndpointUtility.GetCurrentServerTransport();
+            var serverTransport = HttpEndpointUtility.GetCurrentServerTransport();
+
+            Task.Run(() =>
+            {
+                try
+                {
+                    if (client is ClaudeCliMcpConfigurator cliConfigurator)
+                    {
                         cliConfigurator.ConfigureWithCapturedValues(
                             projectDir, claudePath, pathPrepend,
                             useHttpTransport, httpUrl,
                             uvxPath, gitUrl, packageName, shouldForceRefresh,
                             apiKey, serverTransport);
                     }
                     return (success: true, error: (string)null);
MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs (1)

233-297: ⚠️ Potential issue | 🟡 Minor

Avoid implicit type coercion for unknown prefs.

Auto-detecting numeric/bool types from string content can silently coerce values like "00123" or "true" into non-string prefs when a user clicks Save, losing formatting and potentially changing semantics. Consider defaulting unknown keys to String and letting the user explicitly change the type via the dropdown if needed.

💡 Suggested safer default (keep unknown prefs as String)
-                if (int.TryParse(stringValue, out var intValue))
-                {
-                    item.Type = EditorPrefType.Int;
-                    item.Value = intValue.ToString();
-                }
-                else if (float.TryParse(stringValue, out var floatValue))
-                {
-                    item.Type = EditorPrefType.Float;
-                    item.Value = floatValue.ToString();
-                }
-                else if (bool.TryParse(stringValue, out var boolValue))
-                {
-                    item.Type = EditorPrefType.Bool;
-                    item.Value = boolValue.ToString();
-                }
-                else
-                {
-                    item.Type = EditorPrefType.String;
-                    item.Value = stringValue;
-                }
+                item.Type = EditorPrefType.String;
+                item.Value = stringValue;
🤖 Fix all issues with AI agents
In
`@MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs`:
- Around line 531-536: The current mismatch logic marks a transport mismatch
whenever client.ConfiguredTransport differs from the server transport returned
by HttpEndpointUtility.GetCurrentServerTransport(); update the check so that a
mismatch is only set when the serverTransport is known (i.e., not
ConfiguredTransport.Unknown) and the client.ConfiguredTransport is different;
specifically, in the McpClientConfigSection logic that computes
hasTransportMismatch, add a condition to skip setting hasTransportMismatch if
serverTransport == ConfiguredTransport.Unknown (treat Unknown as no mismatch).
🧹 Nitpick comments (1)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (1)

842-878: Consider adding request cancellation support.

The HttpClient usage looks correct with proper using statement and timeout. However, if the Unity Editor closes or the user navigates away during the request, there's no cancellation mechanism.

This is a minor concern since the 10-second timeout provides a reasonable upper bound, and the fire-and-forget nature of the "Get API Key" action is acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add opt-in IP allowlist + bearer token auth for MCP HTTP/WebSocket + tool execution

1 participant