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-20260514-154649.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Bug Fix
body: Fix unstructured tool errors being masked as 'outputSchema defined but no structured output returned'
time: 2026-05-14T15:46:49.572423-07:00
10 changes: 3 additions & 7 deletions src/dbt_mcp/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from dbtlabs_vortex.producer import shutdown
from mcp.server.fastmcp import FastMCP
from mcp.server.lowlevel.server import LifespanResultT
from mcp.types import ContentBlock, TextContent, Tool
from mcp.types import ContentBlock, Tool

from dbt_mcp.config.config import Config
from dbt_mcp.dbt_admin.tools import register_admin_api_tools
Expand Down Expand Up @@ -108,12 +108,8 @@ async def call_tool(
)
except Exception:
logger.debug("Usage tracking failed — skipping", exc_info=True)
return [
TextContent(
type="text",
text=str(e),
)
]
raise

end_time = int(time.time() * 1000)
logger.info(f"Tool {name} called successfully in {end_time - start_time}ms")
try:
Expand Down
20 changes: 7 additions & 13 deletions tests/unit/mcp/test_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ async def test_routes_based_on_project_mode(
mcps[called].call_tool.assert_awaited_once()
mcps[not_called].call_tool.assert_not_awaited()

async def test_returns_text_content_on_tool_error(self):
async def test_raises_on_tool_error(self):
multi = MagicMock(spec=FastMCP)
single = MagicMock(spec=FastMCP)
single.call_tool = AsyncMock(side_effect=RuntimeError("something broke"))
Expand All @@ -202,13 +202,10 @@ async def test_returns_text_content_on_tool_error(self):
with patch.object(
dispatcher, "_is_multi_project", AsyncMock(return_value=False)
):
result = await dispatcher.call_tool("bad_tool", {})
with pytest.raises(RuntimeError, match="something broke"):
await dispatcher.call_tool("bad_tool", {})

assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "something broke" in result[0].text

async def test_tracking_failure_does_not_crash_call_tool(self):
async def test_tracking_failure_does_not_suppress_tool_error(self):
single = MagicMock(spec=FastMCP)
single.call_tool = AsyncMock(
side_effect=MissingHostError("DBT_HOST is a required environment variable")
Expand All @@ -221,13 +218,10 @@ async def test_tracking_failure_does_not_crash_call_tool(self):
with patch.object(
dispatcher, "_is_multi_project", AsyncMock(return_value=False)
):
result = await dispatcher.call_tool("some_tool", {})
with pytest.raises(MissingHostError, match="DBT_HOST"):
await dispatcher.call_tool("some_tool", {})

# Tool error is returned cleanly despite tracking also failing
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "DBT_HOST" in result[0].text
# Tracking was attempted (and failed, but didn't crash)
# Tracking was attempted (and failed, but didn't suppress the tool error)
dispatcher.usage_tracker.emit_tool_called_event.assert_awaited_once()


Expand Down
Loading