From 4a1ffcfd5114bffe741d3a1585a04341ab03ebea Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Mon, 16 Mar 2026 10:36:41 -0400 Subject: [PATCH] feat(auth): add _build_google_auth() builder with stdio warning Adds _build_google_auth() to server.py that reads Google OAuth settings and returns a configured GoogleProvider instance or None when unconfigured. Includes warning for stdio transport incompatibility and conditional jwt_signing_key passthrough. 4 new TDD tests in tests/test_auth_builder.py. --- tests/test_auth_builder.py | 95 ++++++++++++++++++++++++++++++++++++++ unraid_mcp/server.py | 48 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 tests/test_auth_builder.py diff --git a/tests/test_auth_builder.py b/tests/test_auth_builder.py new file mode 100644 index 0000000..3ef8cb8 --- /dev/null +++ b/tests/test_auth_builder.py @@ -0,0 +1,95 @@ +"""Tests for _build_google_auth() in server.py.""" + +import importlib +from unittest.mock import MagicMock, patch + + +def test_build_google_auth_returns_none_when_unconfigured(monkeypatch): + """Returns None when Google OAuth env vars are absent.""" + monkeypatch.delenv("GOOGLE_CLIENT_ID", raising=False) + monkeypatch.delenv("GOOGLE_CLIENT_SECRET", raising=False) + monkeypatch.delenv("UNRAID_MCP_BASE_URL", raising=False) + + import unraid_mcp.config.settings as s + + importlib.reload(s) + + from unraid_mcp.server import _build_google_auth + + result = _build_google_auth() + assert result is None + + +def test_build_google_auth_returns_provider_when_configured(monkeypatch): + """Returns GoogleProvider instance when all required vars are set.""" + monkeypatch.setenv("GOOGLE_CLIENT_ID", "test-id.apps.googleusercontent.com") + monkeypatch.setenv("GOOGLE_CLIENT_SECRET", "GOCSPX-test-secret") + monkeypatch.setenv("UNRAID_MCP_BASE_URL", "http://10.1.0.2:6970") + monkeypatch.setenv("UNRAID_MCP_JWT_SIGNING_KEY", "x" * 32) + + import unraid_mcp.config.settings as s + + importlib.reload(s) + + mock_provider = MagicMock() + mock_provider_class = MagicMock(return_value=mock_provider) + + with patch("unraid_mcp.server.GoogleProvider", mock_provider_class): + from unraid_mcp.server import _build_google_auth + + result = _build_google_auth() + + assert result is mock_provider + mock_provider_class.assert_called_once_with( + client_id="test-id.apps.googleusercontent.com", + client_secret="GOCSPX-test-secret", + base_url="http://10.1.0.2:6970", + jwt_signing_key="x" * 32, + ) + + +def test_build_google_auth_omits_jwt_key_when_empty(monkeypatch): + """jwt_signing_key is omitted (not passed as empty string) when not set.""" + monkeypatch.setenv("GOOGLE_CLIENT_ID", "test-id.apps.googleusercontent.com") + monkeypatch.setenv("GOOGLE_CLIENT_SECRET", "GOCSPX-test-secret") + monkeypatch.setenv("UNRAID_MCP_BASE_URL", "http://10.1.0.2:6970") + monkeypatch.delenv("UNRAID_MCP_JWT_SIGNING_KEY", raising=False) + + import unraid_mcp.config.settings as s + + importlib.reload(s) + + mock_provider_class = MagicMock(return_value=MagicMock()) + + with patch("unraid_mcp.server.GoogleProvider", mock_provider_class): + from unraid_mcp.server import _build_google_auth + + _build_google_auth() + + call_kwargs = mock_provider_class.call_args.kwargs + assert "jwt_signing_key" not in call_kwargs + + +def test_build_google_auth_warns_on_stdio_transport(monkeypatch): + """Logs a warning when Google auth is configured but transport is stdio.""" + monkeypatch.setenv("GOOGLE_CLIENT_ID", "test-id.apps.googleusercontent.com") + monkeypatch.setenv("GOOGLE_CLIENT_SECRET", "GOCSPX-test-secret") + monkeypatch.setenv("UNRAID_MCP_BASE_URL", "http://10.1.0.2:6970") + monkeypatch.setenv("UNRAID_MCP_TRANSPORT", "stdio") + + import unraid_mcp.config.settings as s + + importlib.reload(s) + + warning_messages: list[str] = [] + + from unraid_mcp.server import _build_google_auth + + with ( + patch("unraid_mcp.server.GoogleProvider", MagicMock(return_value=MagicMock())), + patch("unraid_mcp.server.logger") as mock_logger, + ): + mock_logger.warning.side_effect = lambda msg, *a, **kw: warning_messages.append(msg) + _build_google_auth() + + assert any("stdio" in m.lower() for m in warning_messages) diff --git a/unraid_mcp/server.py b/unraid_mcp/server.py index 623b312..bba89dd 100644 --- a/unraid_mcp/server.py +++ b/unraid_mcp/server.py @@ -7,6 +7,7 @@ separate modules for configuration, core functionality, subscriptions, and tools import sys from fastmcp import FastMCP +from fastmcp.server.auth.providers.google import GoogleProvider from fastmcp.server.middleware.caching import CallToolSettings, ResponseCachingMiddleware from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware from fastmcp.server.middleware.logging import LoggingMiddleware @@ -68,6 +69,53 @@ cache_middleware = ResponseCachingMiddleware( get_prompt_settings={"enabled": False}, ) + +def _build_google_auth() -> "GoogleProvider | None": + """Build GoogleProvider when OAuth env vars are configured, else return None. + + Returns None (no auth) when GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET are absent, + preserving backward compatibility for existing unprotected setups. + """ + from .config.settings import ( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + UNRAID_MCP_BASE_URL, + UNRAID_MCP_JWT_SIGNING_KEY, + UNRAID_MCP_TRANSPORT, + is_google_auth_configured, + ) + + if not is_google_auth_configured(): + return None + + if UNRAID_MCP_TRANSPORT == "stdio": + logger.warning( + "Google OAuth is configured but UNRAID_MCP_TRANSPORT=stdio. " + "OAuth requires HTTP transport (streamable-http or sse). " + "Auth will be applied but may not work as expected." + ) + + kwargs: dict[str, str] = { + "client_id": GOOGLE_CLIENT_ID, + "client_secret": GOOGLE_CLIENT_SECRET, + "base_url": UNRAID_MCP_BASE_URL, + } + if UNRAID_MCP_JWT_SIGNING_KEY: + kwargs["jwt_signing_key"] = UNRAID_MCP_JWT_SIGNING_KEY + else: + logger.warning( + "UNRAID_MCP_JWT_SIGNING_KEY is not set. FastMCP will derive a key automatically, " + "but tokens may be invalidated on server restart. " + "Set UNRAID_MCP_JWT_SIGNING_KEY to a stable secret." + ) + + logger.info( + f"Google OAuth enabled — base_url={UNRAID_MCP_BASE_URL}, " + f"redirect_uri={UNRAID_MCP_BASE_URL}/auth/callback" + ) + return GoogleProvider(**kwargs) + + # Initialize FastMCP instance mcp = FastMCP( name="Unraid MCP Server",