Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Bug Fix-20260204-143000.yaml
Original file line number Diff line number Diff line change
@@ -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
19 changes: 13 additions & 6 deletions src/dbt_mcp/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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(
Expand Down
20 changes: 20 additions & 0 deletions src/dbt_mcp/errors/hints.py
Original file line number Diff line number Diff line change
@@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you meant to use this in with_multicell_hint

"""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
3 changes: 3 additions & 0 deletions src/dbt_mcp/oauth/fastapi_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
5 changes: 4 additions & 1 deletion src/dbt_mcp/oauth/token_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()


Expand Down
39 changes: 39 additions & 0 deletions tests/unit/oauth/test_token_refresh_at_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Loading