Skip to content
Merged
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-20260605-102300.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Bug Fix
body: Return dimension-values query failures as tool results instead of raising generic runtime errors.
time: 2026-06-05T10:23:00.932991-05:00
8 changes: 5 additions & 3 deletions src/dbt_mcp/semantic_layer/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from dbt_mcp.semantic_layer.gql.gql_request import submit_request
from dbt_mcp.semantic_layer.types import (
DimensionToolResponse,
DimensionValuesError,
DimensionValuesResult,
DimensionValuesResponse,
EntityToolResponse,
GetMetricsCompiledSqlError,
Expand Down Expand Up @@ -363,7 +365,7 @@ async def get_dimension_values(
dimension: str,
metrics: list[str] | None = None,
limit: int = 100,
) -> DimensionValuesResponse:
) -> DimensionValuesResult:
try:
sl_client = await self.client_provider.get_client(config=config)

Expand Down Expand Up @@ -392,8 +394,8 @@ def query() -> pa.Table:
]
truncated = len(raw) > limit
return DimensionValuesResponse(values=raw[:limit], truncated=truncated)
except Exception as e:
raise RuntimeError(self._format_semantic_layer_error(e)) from e
except QueryFailedError as e:
return DimensionValuesError(error=self._format_semantic_layer_error(e))

def _format_semantic_layer_error(self, error: Exception) -> str:
"""Format semantic layer errors by cleaning up common error message patterns."""
Expand Down
3 changes: 2 additions & 1 deletion src/dbt_mcp/semantic_layer/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
SEMANTIC_WHERE,
)
from dbt_mcp.semantic_layer.types import (
DimensionValuesError,
DimensionToolResponse,
DimensionValuesResponse,
EntityToolResponse,
Expand Down Expand Up @@ -218,7 +219,7 @@ async def get_dimension_values(
limit: Annotated[
int, Field(ge=1, description=SEMANTIC_DIMENSION_VALUES_LIMIT)
] = 100,
) -> DimensionValuesResponse:
) -> DimensionValuesResponse | DimensionValuesError:
config = await context.config_provider.get_config()
return await context.semantic_layer_fetcher.get_dimension_values(
config=config,
Expand Down
3 changes: 2 additions & 1 deletion src/dbt_mcp/semantic_layer/tools_multiproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)
from dbt_mcp.semantic_layer.tools import metrics_to_csv
from dbt_mcp.semantic_layer.types import (
DimensionValuesError,
DimensionToolResponse,
DimensionValuesResponse,
EntityToolResponse,
Expand Down Expand Up @@ -234,7 +235,7 @@ async def get_dimension_values(
limit: Annotated[
int, Field(ge=1, description=SEMANTIC_DIMENSION_VALUES_LIMIT)
] = 100,
) -> DimensionValuesResponse:
) -> DimensionValuesResponse | DimensionValuesError:
config = await context.semantic_layer_config_provider.get_config(project_id)
return await SemanticLayerFetcher(
client_provider=context.client_provider
Expand Down
11 changes: 11 additions & 0 deletions src/dbt_mcp/semantic_layer/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,14 @@ class GetMetricsCompiledSqlError:
class DimensionValuesResponse:
values: list[str]
truncated: bool
error: None = None


@dataclass
class DimensionValuesError:
error: str
values: None = None
truncated: None = None


DimensionValuesResult = DimensionValuesResponse | DimensionValuesError
30 changes: 30 additions & 0 deletions tests/unit/semantic_layer/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from dbt_mcp.errors.semantic_layer import SemanticLayerQueryTimeoutError
from dbt_mcp.semantic_layer.client import DEFAULT_RESULT_FORMATTER, SemanticLayerFetcher
from dbt_mcp.semantic_layer.types import (
DimensionValuesError,
DimensionValuesResponse,
OrderByParam,
QueryMetricsError,
Expand Down Expand Up @@ -1069,6 +1070,35 @@ async def test_omits_nulls(self, dim_fetcher, mock_sl_client, sl_config):
assert result.values == ["US", "FR"]
assert result.truncated is False

@pytest.mark.asyncio
async def test_query_failed_error_returns_error_result(
self, dim_fetcher, mock_sl_client, sl_config
):
mock_sl_client.dimension_values.side_effect = QueryFailedError(
"Dimension 'foo' not found", "FAILED"
)

result = await dim_fetcher.get_dimension_values(
config=sl_config, dimension="foo", metrics=["revenue"], limit=100
)

assert isinstance(result, DimensionValuesError)
assert "foo" in result.error

@pytest.mark.asyncio
async def test_operational_error_propagates(
self, dim_fetcher, mock_sl_client, sl_config
):
mock_sl_client.dimension_values.side_effect = AuthError("invalid credentials")

with pytest.raises(AuthError):
await dim_fetcher.get_dimension_values(
config=sl_config,
dimension="customer__country",
metrics=["revenue"],
limit=100,
)


class TestSessionRunsOffEventLoop:
"""Opening a Semantic Layer session is blocking I/O (it establishes a
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/semantic_layer/test_dimension_values_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock

import pytest

from dbt_mcp.semantic_layer.tools import get_dimension_values
from dbt_mcp.semantic_layer.types import DimensionValuesError, DimensionValuesResponse


@pytest.fixture
def tool_context():
config = object()
return SimpleNamespace(
config_provider=SimpleNamespace(get_config=AsyncMock(return_value=config)),
semantic_layer_fetcher=SimpleNamespace(get_dimension_values=AsyncMock()),
)


@pytest.mark.asyncio
async def test_get_dimension_values_tool_returns_success_response(tool_context):
response = DimensionValuesResponse(values=["US", "UK"], truncated=False)
tool_context.semantic_layer_fetcher.get_dimension_values.return_value = response

result = await get_dimension_values.fn(
context=tool_context,
dimension="customer__country",
metrics=["revenue"],
limit=100,
)

assert result is response


@pytest.mark.asyncio
async def test_get_dimension_values_tool_returns_error_response(tool_context):
response = DimensionValuesError(error="Dimension 'foo' not found")
tool_context.semantic_layer_fetcher.get_dimension_values.return_value = response

result = await get_dimension_values.fn(
context=tool_context,
dimension="foo",
metrics=["revenue"],
limit=100,
)

assert result is response
Loading