diff --git a/.changes/unreleased/Bug Fix-20260204-143000.yaml b/.changes/unreleased/Bug Fix-20260204-143000.yaml new file mode 100644 index 00000000..f8a5ddf1 --- /dev/null +++ b/.changes/unreleased/Bug Fix-20260204-143000.yaml @@ -0,0 +1,3 @@ +kind: Bug Fix +body: Add helpful hint for SSL errors suggesting MULTICELL_ACCOUNT_PREFIX configuration +time: 2026-02-04T14:30:00.000000+01:00 diff --git a/src/dbt_mcp/config/settings.py b/src/dbt_mcp/config/settings.py index ac42eb9b..4ad240fb 100644 --- a/src/dbt_mcp/config/settings.py +++ b/src/dbt_mcp/config/settings.py @@ -24,6 +24,7 @@ dbt_platform_context_from_token_response, ) from dbt_mcp.oauth.login import login +from dbt_mcp.errors.hints import with_multicell_hint from dbt_mcp.oauth.token_provider import ( OAuthTokenProvider, StaticTokenProvider, @@ -417,7 +418,9 @@ def _try_refresh_token( logger.info("Successfully refreshed access token at startup") return updated_context except Exception as e: - logger.warning(f"Failed to refresh token at startup: {e}") + logger.warning( + f"Failed to refresh token at startup: {with_multicell_hint(str(e))}" + ) return None @@ -447,11 +450,15 @@ async def get_dbt_platform_context( # Fall back to full OAuth login flow selected_port = _find_available_port(start_port=OAUTH_REDIRECT_STARTING_PORT) - return await login( - dbt_platform_url=dbt_platform_url, - port=selected_port, - dbt_platform_context_manager=dbt_platform_context_manager, - ) + try: + return await login( + dbt_platform_url=dbt_platform_url, + port=selected_port, + dbt_platform_context_manager=dbt_platform_context_manager, + ) + except Exception as e: + # Add helpful hint for SSL errors (common with multi-cell misconfiguration) + raise type(e)(with_multicell_hint(str(e))) from e def get_dbt_host( diff --git a/src/dbt_mcp/errors/hints.py b/src/dbt_mcp/errors/hints.py new file mode 100644 index 00000000..156b667f --- /dev/null +++ b/src/dbt_mcp/errors/hints.py @@ -0,0 +1,20 @@ +"""Helpful hints for common error conditions.""" + +MULTICELL_HINT = ( + "Hint: If you are on a multi-cell dbt platform instance, you may need to set " + "the MULTICELL_ACCOUNT_PREFIX environment variable. " + "See https://docs.getdbt.com/docs/dbt-ai/setup-local-mcp#api-and-sql-tool-settings" +) + + +def looks_like_ssl_error(error: Exception) -> bool: + """Check if an exception looks like an SSL/certificate error.""" + error_str = str(error).lower() + return any(kw in error_str for kw in ["ssl", "certificate"]) + + +def with_multicell_hint(message: str) -> str: + """Add multicell hint to a message if it looks like an SSL error.""" + if any(kw in message.lower() for kw in ["ssl", "certificate"]): + return f"{message}\n\n{MULTICELL_HINT}" + return message diff --git a/src/dbt_mcp/oauth/fastapi_app.py b/src/dbt_mcp/oauth/fastapi_app.py index 0d4ff840..6533ad2f 100644 --- a/src/dbt_mcp/oauth/fastapi_app.py +++ b/src/dbt_mcp/oauth/fastapi_app.py @@ -10,6 +10,7 @@ from starlette.types import Receive, Scope, Send from uvicorn import Server +from dbt_mcp.errors.hints import with_multicell_hint from dbt_mcp.oauth.context_manager import DbtPlatformContextManager from dbt_mcp.oauth.dbt_platform import ( DbtPlatformAccount, @@ -184,6 +185,8 @@ def oauth_callback(request: Request) -> RedirectResponse: logger.exception("OAuth callback failed") default_msg = "An unexpected error occurred during authentication" error_message = str(e) if str(e) else default_msg + # Add hint for SSL errors (common with multi-cell misconfiguration) + error_message = with_multicell_hint(error_message) return error_redirect("oauth_failed", error_message) @app.post("/shutdown") diff --git a/src/dbt_mcp/oauth/token_provider.py b/src/dbt_mcp/oauth/token_provider.py index 1762245c..d0c92cbf 100644 --- a/src/dbt_mcp/oauth/token_provider.py +++ b/src/dbt_mcp/oauth/token_provider.py @@ -4,6 +4,7 @@ from authlib.integrations.requests_client import OAuth2Session +from dbt_mcp.errors.hints import with_multicell_hint from dbt_mcp.oauth.client_id import OAUTH_CLIENT_ID from dbt_mcp.oauth.context_manager import DbtPlatformContextManager from dbt_mcp.oauth.dbt_platform import dbt_platform_context_from_token_response @@ -85,7 +86,9 @@ async def _background_refresh_worker(self) -> None: ) await self._refresh_token() except Exception as e: - logger.error(f"Error in background refresh worker: {e}") + # Add hint for SSL errors (common with multi-cell misconfiguration) + error_msg = with_multicell_hint(str(e)) + logger.error(f"Error in background refresh worker: {error_msg}") await self.refresh_strategy.wait_after_error() diff --git a/tests/unit/oauth/test_token_refresh_at_startup.py b/tests/unit/oauth/test_token_refresh_at_startup.py index eac11ad8..2fd6f5eb 100644 --- a/tests/unit/oauth/test_token_refresh_at_startup.py +++ b/tests/unit/oauth/test_token_refresh_at_startup.py @@ -6,6 +6,7 @@ _is_token_valid, _try_refresh_token, ) +from dbt_mcp.errors.hints import looks_like_ssl_error, with_multicell_hint from dbt_mcp.oauth.dbt_platform import ( DbtPlatformContext, DbtPlatformEnvironment, @@ -190,3 +191,41 @@ def test_no_token_returns_none(self): ) assert result is None + + +class TestSslErrorDetection: + """Tests for SSL error detection and message enhancement.""" + + def test_looks_like_ssl_error_with_ssl_keyword(self): + """Errors containing 'ssl' should be detected.""" + error = Exception("SSLError: certificate verify failed") + assert looks_like_ssl_error(error) is True + + def test_looks_like_ssl_error_with_certificate_keyword(self): + """Errors containing 'certificate' should be detected.""" + error = Exception("certificate verify failed") + assert looks_like_ssl_error(error) is True + + def test_looks_like_ssl_error_case_insensitive(self): + """Detection should be case-insensitive.""" + error = Exception("SSL_CERTIFICATE_ERROR") + assert looks_like_ssl_error(error) is True + + def test_looks_like_ssl_error_false_for_other_errors(self): + """Non-SSL errors should not be detected.""" + error = Exception("Connection refused") + assert looks_like_ssl_error(error) is False + + def test_with_multicell_hint_adds_hint_for_ssl_error(self): + """SSL errors should get the MULTICELL_ACCOUNT_PREFIX hint.""" + message = "SSLError: certificate verify failed" + enhanced = with_multicell_hint(message) + assert "MULTICELL_ACCOUNT_PREFIX" in enhanced + assert "SSLError" in enhanced + + def test_with_multicell_hint_no_hint_for_other_errors(self): + """Non-SSL errors should not get the hint.""" + message = "Connection refused" + enhanced = with_multicell_hint(message) + assert "MULTICELL_ACCOUNT_PREFIX" not in enhanced + assert enhanced == "Connection refused"