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.
This commit is contained in:
Jacob Magar
2026-03-16 10:36:41 -04:00
parent f69aa94826
commit 4a1ffcfd51
2 changed files with 143 additions and 0 deletions

View File

@@ -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)

View File

@@ -7,6 +7,7 @@ separate modules for configuration, core functionality, subscriptions, and tools
import sys import sys
from fastmcp import FastMCP from fastmcp import FastMCP
from fastmcp.server.auth.providers.google import GoogleProvider
from fastmcp.server.middleware.caching import CallToolSettings, ResponseCachingMiddleware from fastmcp.server.middleware.caching import CallToolSettings, ResponseCachingMiddleware
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
from fastmcp.server.middleware.logging import LoggingMiddleware from fastmcp.server.middleware.logging import LoggingMiddleware
@@ -68,6 +69,53 @@ cache_middleware = ResponseCachingMiddleware(
get_prompt_settings={"enabled": False}, 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 # Initialize FastMCP instance
mcp = FastMCP( mcp = FastMCP(
name="Unraid MCP Server", name="Unraid MCP Server",