Skip to content

Commit 6c4ae85

Browse files
committed
fix: improve error messages for platform credential expiry in Semantic Layer tools
When query_metrics or execute_sql fail due to expired dbt Cloud platform credentials, the error now clearly indicates this is a platform-side issue and directs users to refresh credentials in the dbt Cloud UI, rather than suggesting local Snowflake re-authentication. Closes #670
1 parent 62a496f commit 6c4ae85

3 files changed

Lines changed: 173 additions & 2 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Fixed
2+
body: Improve error messages when dbt Cloud platform credentials expire for Semantic Layer tools (query_metrics, execute_sql). The error now clearly indicates this is a platform-side credential issue and directs users to the dbt Cloud UI to refresh their development credentials.
3+
time: 2026-04-12T21:00:00.000000-07:00

src/dbt_mcp/semantic_layer/client.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import base64
33
import json
4+
import re
45
from collections.abc import Callable
56
from contextlib import AbstractContextManager
67
from datetime import date, datetime, time, timedelta
@@ -38,6 +39,36 @@
3839
ListMetricsResponse,
3940
)
4041

42+
# Patterns that indicate expired dbt Cloud platform credentials rather than
43+
# local authentication failures. These are matched case-insensitively against
44+
# the raw error string returned by the Semantic Layer / data platform.
45+
_PLATFORM_CREDENTIAL_ERROR_PATTERNS: list[re.Pattern[str]] = [
46+
re.compile(r"SSO authentication has expired", re.IGNORECASE),
47+
re.compile(r"re-connect to Snowflake", re.IGNORECASE),
48+
re.compile(r"refresh.snowflake.oauth.credentials", re.IGNORECASE),
49+
re.compile(r"authentication token has expired", re.IGNORECASE),
50+
re.compile(r"oauth.*token.*expired", re.IGNORECASE),
51+
re.compile(r"token.*expired.*re-?authenticate", re.IGNORECASE),
52+
]
53+
54+
_PLATFORM_CREDENTIAL_HINT = (
55+
"\n\n"
56+
"Hint: This error indicates that your dbt Cloud development credentials "
57+
"have expired. This is a dbt Cloud platform credential issue, not a local "
58+
"authentication problem.\n"
59+
"To fix this, go to the dbt Cloud UI → Profile → Credentials and refresh "
60+
"your development environment credentials for the relevant project.\n"
61+
"See: https://docs.getdbt.com/docs/dbt-cloud-environments/develop-in-the-cloud#access-the-cloud-ide"
62+
)
63+
64+
65+
def _is_platform_credential_error(error_message: str) -> bool:
66+
"""Detect whether an error message indicates expired dbt Cloud platform credentials."""
67+
return any(
68+
pattern.search(error_message)
69+
for pattern in _PLATFORM_CREDENTIAL_ERROR_PATTERNS
70+
)
71+
4172

4273
def DEFAULT_RESULT_FORMATTER(table: pa.Table) -> str:
4374
"""Convert PyArrow Table to JSON string with ISO date formatting.
@@ -268,7 +299,12 @@ async def get_entities(
268299
return self.entities_cache[metrics_key]
269300

270301
def _format_semantic_layer_error(self, error: Exception) -> str:
271-
"""Format semantic layer errors by cleaning up common error message patterns."""
302+
"""Format semantic layer errors by cleaning up common error message patterns.
303+
304+
Additionally detects errors caused by expired dbt Cloud platform credentials
305+
and appends an actionable hint directing users to refresh their credentials
306+
in the dbt Cloud UI rather than re-authenticating locally.
307+
"""
272308
error_str = str(error)
273309
formatted = (
274310
error_str.replace("QueryFailedError(", "")
@@ -285,7 +321,14 @@ def _format_semantic_layer_error(self, error: Exception) -> str:
285321
.strip()
286322
)
287323
if not formatted:
288-
return error_str or f"Semantic layer query failed: {type(error).__name__}"
324+
formatted = (
325+
error_str or f"Semantic layer query failed: {type(error).__name__}"
326+
)
327+
328+
# Detect platform credential expiry and append an actionable hint
329+
if _is_platform_credential_error(error_str):
330+
formatted += _PLATFORM_CREDENTIAL_HINT
331+
289332
return formatted
290333

291334
def _format_get_metrics_compiled_sql_error(
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Tests for platform credential error detection in Semantic Layer tools.
2+
3+
Verifies that when query_metrics or get_metrics_compiled_sql fail due to
4+
expired dbt Cloud platform credentials, the error message clearly indicates
5+
this is a platform-side issue and directs users to the dbt Cloud UI.
6+
7+
Related: https://github.com/dbt-labs/dbt-mcp/issues/670
8+
"""
9+
10+
import pytest
11+
12+
from dbt_mcp.semantic_layer.client import (
13+
SemanticLayerFetcher,
14+
_PLATFORM_CREDENTIAL_HINT,
15+
_is_platform_credential_error,
16+
)
17+
18+
19+
@pytest.fixture
20+
def fetcher():
21+
from unittest.mock import AsyncMock
22+
23+
return SemanticLayerFetcher(client_provider=AsyncMock())
24+
25+
26+
class TestPlatformCredentialErrorDetection:
27+
"""Tests for _is_platform_credential_error."""
28+
29+
def test_detects_sso_authentication_expired(self) -> None:
30+
assert _is_platform_credential_error(
31+
"SSO authentication has expired, please re-connect to Snowflake"
32+
)
33+
34+
def test_detects_reconnect_to_snowflake(self) -> None:
35+
assert _is_platform_credential_error(
36+
"Please re-connect to Snowflake: https://docs.getdbt.com/faqs"
37+
)
38+
39+
def test_detects_refresh_snowflake_oauth_url(self) -> None:
40+
assert _is_platform_credential_error(
41+
"https://docs.getdbt.com/faqs/Troubleshooting/refresh-snowflake-oauth-credentials"
42+
)
43+
44+
def test_detects_authentication_token_expired(self) -> None:
45+
assert _is_platform_credential_error(
46+
"Authentication token has expired for the data warehouse"
47+
)
48+
49+
def test_detects_oauth_token_expired(self) -> None:
50+
assert _is_platform_credential_error(
51+
"OAuth access token has expired for this connection"
52+
)
53+
54+
def test_case_insensitive(self) -> None:
55+
assert _is_platform_credential_error(
56+
"sso AUTHENTICATION Has Expired"
57+
)
58+
59+
def test_does_not_match_generic_query_error(self) -> None:
60+
assert not _is_platform_credential_error(
61+
"column 'revenue' not found in table"
62+
)
63+
64+
def test_does_not_match_syntax_error(self) -> None:
65+
assert not _is_platform_credential_error(
66+
"SQL compilation error: syntax error at position 42"
67+
)
68+
69+
def test_does_not_match_timeout_error(self) -> None:
70+
assert not _is_platform_credential_error(
71+
"Query timed out after 60 seconds"
72+
)
73+
74+
def test_does_not_match_empty_string(self) -> None:
75+
assert not _is_platform_credential_error("")
76+
77+
78+
class TestFormatSemanticLayerErrorWithCredentialHint:
79+
"""Tests that _format_semantic_layer_error appends the credential hint."""
80+
81+
def test_sso_expired_includes_platform_hint(self, fetcher) -> None:
82+
error = Exception(
83+
"SSO authentication has expired, please re-connect to Snowflake: "
84+
"https://docs.getdbt.com/faqs/Troubleshooting/refresh-snowflake-oauth-credentials"
85+
)
86+
result = fetcher._format_semantic_layer_error(error)
87+
assert "dbt Cloud" in result
88+
assert "Profile → Credentials" in result
89+
assert "not a local authentication problem" in result
90+
91+
def test_sso_expired_preserves_original_message(self, fetcher) -> None:
92+
error = Exception(
93+
"SSO authentication has expired, please re-connect to Snowflake"
94+
)
95+
result = fetcher._format_semantic_layer_error(error)
96+
assert "SSO authentication has expired" in result
97+
98+
def test_generic_error_does_not_include_platform_hint(self, fetcher) -> None:
99+
error = Exception("column 'revenue' not found")
100+
result = fetcher._format_semantic_layer_error(error)
101+
assert _PLATFORM_CREDENTIAL_HINT not in result
102+
assert "dbt Cloud UI" not in result
103+
104+
def test_query_failed_error_with_credential_message_includes_hint(
105+
self, fetcher
106+
) -> None:
107+
"""QueryFailedError wrapping a credential expiry should also get the hint."""
108+
error = Exception(
109+
'QueryFailedError(["SSO authentication has expired, '
110+
"please re-connect to Snowflake\"])"
111+
)
112+
result = fetcher._format_semantic_layer_error(error)
113+
assert "dbt Cloud" in result
114+
assert "Profile → Credentials" in result
115+
116+
def test_format_query_failed_error_with_credential_message(self, fetcher) -> None:
117+
"""_format_query_failed_error should also include the hint for credential errors."""
118+
from dbtsl.error import QueryFailedError
119+
120+
error = QueryFailedError(
121+
"SSO authentication has expired, please re-connect to Snowflake"
122+
)
123+
result = fetcher._format_query_failed_error(error)
124+
assert "dbt Cloud" in result.error
125+
assert "Profile → Credentials" in result.error

0 commit comments

Comments
 (0)