Skip to content

Commit 122220c

Browse files
authored
Tool policies (#263)
dbt Labs' policies dictate that we do not feed row-level data to LLMs in our systems. There is no such restriction for typical users of dbt-mcp because they have their own terms with LLM providers. However, when we use dbt-mcp internally, we need to ensure that we don't send tool results with row-level data to LLMs. This PR applies policies to each tool to ensure we are always compliant. I had to change the get_model_details tool to return raw SQL instead of compiled SQL in order for it to be compliant with metadata only.
1 parent 901a0dd commit 122220c

5 files changed

Lines changed: 154 additions & 9 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Enhancement or New Feature
2+
body: 'Tool policies '
3+
time: 2025-07-30T20:26:22.429381-05:00

src/dbt_mcp/discovery/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class GraphQLQueries:
5050
node {
5151
name
5252
uniqueId
53-
compiledCode
53+
rawCode
5454
description
5555
database
5656
schema

src/dbt_mcp/tools/policy.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from enum import Enum
2+
3+
from pydantic.dataclasses import dataclass
4+
5+
from dbt_mcp.tools.tool_names import ToolName
6+
7+
8+
class ToolBehavior(Enum):
9+
"""Behavior of the tool."""
10+
11+
# The tool can return row-level data.
12+
RESULT_SET = "result_set"
13+
# The tool only returns metadata.
14+
METADATA = "metadata"
15+
16+
17+
@dataclass
18+
class ToolPolicy:
19+
"""Policy for a tool."""
20+
21+
name: str
22+
behavior: ToolBehavior
23+
24+
25+
# Defining tool policies is important for our internal usage of dbt-mcp.
26+
# Our policies dictate that we do not send row-level data to LLMs.
27+
tool_policies = {
28+
# CLI tools
29+
ToolName.SHOW.value: ToolPolicy(
30+
name=ToolName.SHOW.value, behavior=ToolBehavior.RESULT_SET
31+
),
32+
ToolName.LIST.value: ToolPolicy(
33+
name=ToolName.LIST.value, behavior=ToolBehavior.METADATA
34+
),
35+
ToolName.DOCS.value: ToolPolicy(
36+
name=ToolName.DOCS.value, behavior=ToolBehavior.METADATA
37+
),
38+
# Compile tool can have result_set behavior because of macros like print_table
39+
ToolName.COMPILE.value: ToolPolicy(
40+
name=ToolName.COMPILE.value, behavior=ToolBehavior.RESULT_SET
41+
),
42+
ToolName.TEST.value: ToolPolicy(
43+
name=ToolName.TEST.value, behavior=ToolBehavior.METADATA
44+
),
45+
# Run tool can have result_set behavior because of macros like print_table
46+
ToolName.RUN.value: ToolPolicy(
47+
name=ToolName.RUN.value, behavior=ToolBehavior.RESULT_SET
48+
),
49+
# Build tool can have result_set behavior because of macros like print_table
50+
ToolName.BUILD.value: ToolPolicy(
51+
name=ToolName.BUILD.value, behavior=ToolBehavior.RESULT_SET
52+
),
53+
ToolName.PARSE.value: ToolPolicy(
54+
name=ToolName.PARSE.value, behavior=ToolBehavior.METADATA
55+
),
56+
# Semantic Layer tools
57+
ToolName.LIST_METRICS.value: ToolPolicy(
58+
name=ToolName.LIST_METRICS.value, behavior=ToolBehavior.METADATA
59+
),
60+
ToolName.GET_DIMENSIONS.value: ToolPolicy(
61+
name=ToolName.GET_DIMENSIONS.value, behavior=ToolBehavior.METADATA
62+
),
63+
ToolName.GET_ENTITIES.value: ToolPolicy(
64+
name=ToolName.GET_ENTITIES.value, behavior=ToolBehavior.METADATA
65+
),
66+
ToolName.QUERY_METRICS.value: ToolPolicy(
67+
name=ToolName.QUERY_METRICS.value, behavior=ToolBehavior.RESULT_SET
68+
),
69+
ToolName.GET_METRICS_COMPILED_SQL.value: ToolPolicy(
70+
name=ToolName.GET_METRICS_COMPILED_SQL.value, behavior=ToolBehavior.METADATA
71+
),
72+
# Discovery tools
73+
ToolName.GET_MODEL_PARENTS.value: ToolPolicy(
74+
name=ToolName.GET_MODEL_PARENTS.value, behavior=ToolBehavior.METADATA
75+
),
76+
ToolName.GET_MODEL_CHILDREN.value: ToolPolicy(
77+
name=ToolName.GET_MODEL_CHILDREN.value, behavior=ToolBehavior.METADATA
78+
),
79+
ToolName.GET_MODEL_DETAILS.value: ToolPolicy(
80+
name=ToolName.GET_MODEL_DETAILS.value, behavior=ToolBehavior.METADATA
81+
),
82+
ToolName.GET_MART_MODELS.value: ToolPolicy(
83+
name=ToolName.GET_MART_MODELS.value, behavior=ToolBehavior.METADATA
84+
),
85+
ToolName.GET_ALL_MODELS.value: ToolPolicy(
86+
name=ToolName.GET_ALL_MODELS.value, behavior=ToolBehavior.METADATA
87+
),
88+
# SQL tools
89+
ToolName.TEXT_TO_SQL.value: ToolPolicy(
90+
name=ToolName.TEXT_TO_SQL.value, behavior=ToolBehavior.METADATA
91+
),
92+
ToolName.EXECUTE_SQL.value: ToolPolicy(
93+
name=ToolName.EXECUTE_SQL.value, behavior=ToolBehavior.RESULT_SET
94+
),
95+
}

tests/unit/tools/test_tool_names.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,18 @@
99
@pytest.mark.asyncio
1010
async def test_tool_names_match_server_tools():
1111
"""Test that the ToolName enum matches the tools registered in the server."""
12+
sql_tool_names = {"text_to_sql", "execute_sql"}
13+
1214
with default_env_vars_context():
1315
config = load_config()
1416
dbt_mcp = await create_dbt_mcp(config)
1517

1618
# Get all tools from the server
1719
server_tools = await dbt_mcp.list_tools()
18-
server_tool_names = {tool.name for tool in server_tools}
19-
enum_names = {
20-
n
21-
for n in ToolName.get_all_tool_names()
22-
if n
23-
# Not testing SQL tools for now
24-
not in ["text_to_sql", "execute_sql"]
25-
}
20+
# Manually adding SQL tools here because the server doesn't get them
21+
# in this unit test.
22+
server_tool_names = {tool.name for tool in server_tools} | sql_tool_names
23+
enum_names = {n for n in ToolName.get_all_tool_names()}
2624

2725
# This should not raise any errors if the enum is in sync
2826
if server_tool_names != enum_names:
@@ -37,3 +35,8 @@ async def test_tool_names_match_server_tools():
3735
assert isinstance(tool.value, str), (
3836
f"Tool {tool.name} value should be a string"
3937
)
38+
39+
40+
def test_tool_names_no_duplicates():
41+
"""Test that there are no duplicate tool names in the enum."""
42+
assert len(ToolName.get_all_tool_names()) == len(set(ToolName.get_all_tool_names()))
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from dbt_mcp.config.config import load_config
2+
from dbt_mcp.mcp.server import create_dbt_mcp
3+
from dbt_mcp.tools.policy import tool_policies
4+
from dbt_mcp.tools.tool_names import ToolName
5+
from tests.env_vars import default_env_vars_context
6+
7+
8+
async def test_tool_policies_match_server_tools():
9+
"""Test that the ToolPolicy enum matches the tools registered in the server."""
10+
sql_tool_names = {"text_to_sql", "execute_sql"}
11+
12+
with default_env_vars_context():
13+
config = load_config()
14+
dbt_mcp = await create_dbt_mcp(config)
15+
16+
# Get all tools from the server
17+
server_tools = await dbt_mcp.list_tools()
18+
# Manually adding SQL tools here because the server doesn't get them
19+
# in this unit test.
20+
server_tool_names = {tool.name for tool in server_tools} | sql_tool_names
21+
policy_names = {policy_name for policy_name in tool_policies}
22+
23+
if server_tool_names != policy_names:
24+
raise ValueError(
25+
f"Tool name mismatch:\n"
26+
f"In server but not in enum: {server_tool_names - policy_names}\n"
27+
f"In enum but not in server: {policy_names - server_tool_names}"
28+
)
29+
30+
31+
def test_tool_policies_match_tool_names():
32+
policy_names = {policy.upper() for policy in tool_policies}
33+
tool_names = {tool.name for tool in ToolName}
34+
if tool_names != policy_names:
35+
raise ValueError(
36+
f"Tool name mismatch:\n"
37+
f"In tool names but not in policy: {tool_names - policy_names}\n"
38+
f"In policy but not in tool names: {policy_names - tool_names}"
39+
)
40+
41+
42+
def test_tool_policies_no_duplicates():
43+
"""Test that there are no duplicate tool names in the policy."""
44+
assert len(tool_policies) == len(set(tool_policies.keys()))

0 commit comments

Comments
 (0)