Skip to content

Commit 70803e5

Browse files
feat: Add list saved queries tool for Semantic Layer (#426)
### Summary This PR adds support for listing saved queries in the dbt Semantic Layer. Saved queries are pre-defined MetricFlow configurations that group related metrics, dimensions, and filters, making them discoverable and reusable through the MCP server. ### Why Because saved_queries is not implemented yet ## Related Issues Closes #264 ## Reference https://docs.getdbt.com/docs/dbt-cloud-apis/sl-graphql#list-saved-queries --------- Co-authored-by: Devon Fulcher <24593113+DevonFulcher@users.noreply.github.com>
1 parent 8b7f923 commit 70803e5

11 files changed

Lines changed: 277 additions & 0 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: Add list_saved_queries tool to the Semantic Layer, enabling discovery of predefined MetricFlow queries via GraphQL API. This allows AI agents to list and search saved queries with their associated metrics, dimensions, and filters.
3+
time: 2025-11-01T07:22:30.000000+09:00
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
List all saved queries from the dbt Semantic Layer.
2+
3+
Saved queries are pre-defined queries that have been saved in the Semantic Layer.
4+
They contain specific metrics, group-by dimensions, and filters that are commonly used.
5+
This tool helps discover what predefined queries are available for quick execution.
6+
7+
Use this tool when:
8+
- The user wants to see what pre-built queries are available
9+
- The user mentions "saved queries" or "predefined queries"
10+
- You need to find commonly used analytical queries
11+
12+
Examples:
13+
- "What saved queries are available?"
14+
- "Show me the predefined reports"
15+
- "List all saved queries for revenue analysis"
16+
17+
<parameters>
18+
search: Optional string used to filter saved queries by name, label, or description using partial matches
19+
</parameters>

src/dbt_mcp/semantic_layer/client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
QueryMetricsError,
2929
QueryMetricsResult,
3030
QueryMetricsSuccess,
31+
SavedQueryToolResponse,
3132
)
3233

3334

@@ -103,6 +104,39 @@ async def list_metrics(self, search: str | None = None) -> list[MetricToolRespon
103104
for m in metrics_result["data"]["metricsPaginated"]["items"]
104105
]
105106

107+
async def list_saved_queries(
108+
self, search: str | None = None
109+
) -> list[SavedQueryToolResponse]:
110+
"""Fetch all saved queries from the Semantic Layer API."""
111+
saved_queries_result = submit_request(
112+
await self.config_provider.get_config(),
113+
{
114+
"query": GRAPHQL_QUERIES["saved_queries"],
115+
"variables": {"search": search},
116+
},
117+
)
118+
return [
119+
SavedQueryToolResponse(
120+
name=sq.get("name"),
121+
label=sq.get("label"),
122+
description=sq.get("description"),
123+
metrics=[
124+
m.get("name") for m in sq.get("queryParams", {}).get("metrics", [])
125+
]
126+
if sq.get("queryParams", {}).get("metrics")
127+
else None,
128+
group_by=[
129+
g.get("name") for g in sq.get("queryParams", {}).get("groupBy", [])
130+
]
131+
if sq.get("queryParams", {}).get("groupBy")
132+
else None,
133+
where=sq.get("queryParams", {}).get("where", {}).get("whereSqlTemplate")
134+
if sq.get("queryParams", {}).get("where")
135+
else None,
136+
)
137+
for sq in saved_queries_result["data"]["savedQueriesPaginated"]["items"]
138+
]
139+
106140
async def get_dimensions(
107141
self, metrics: list[str], search: str | None = None
108142
) -> list[DimensionToolResponse]:

src/dbt_mcp/semantic_layer/gql/gql.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@
3838
type
3939
}
4040
}
41+
}
42+
""",
43+
"saved_queries": """
44+
query GetSavedQueries($environmentId: BigInt!, $search: String) {
45+
savedQueriesPaginated(environmentId: $environmentId, search: $search) {
46+
items {
47+
name
48+
description
49+
label
50+
queryParams {
51+
metrics {
52+
name
53+
}
54+
groupBy {
55+
name
56+
grain
57+
datePart
58+
}
59+
where {
60+
whereSqlTemplate
61+
}
62+
}
63+
}
64+
}
4165
}
4266
""",
4367
}

src/dbt_mcp/semantic_layer/tools.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
MetricToolResponse,
2121
OrderByParam,
2222
QueryMetricsSuccess,
23+
SavedQueryToolResponse,
2324
)
2425
from dbt_mcp.tools.annotations import create_tool_annotations
2526
from dbt_mcp.tools.definitions import ToolDefinition
@@ -41,6 +42,11 @@ def create_sl_tool_definitions(
4142
async def list_metrics(search: str | None = None) -> list[MetricToolResponse]:
4243
return await semantic_layer_fetcher.list_metrics(search=search)
4344

45+
async def list_saved_queries(
46+
search: str | None = None,
47+
) -> list[SavedQueryToolResponse]:
48+
return await semantic_layer_fetcher.list_saved_queries(search=search)
49+
4450
async def get_dimensions(
4551
metrics: list[str], search: str | None = None
4652
) -> list[DimensionToolResponse]:
@@ -102,6 +108,16 @@ async def get_metrics_compiled_sql(
102108
idempotent_hint=True,
103109
),
104110
),
111+
ToolDefinition(
112+
description=get_prompt("semantic_layer/list_saved_queries"),
113+
fn=list_saved_queries,
114+
annotations=create_tool_annotations(
115+
title="List Saved Queries",
116+
read_only_hint=True,
117+
destructive_hint=False,
118+
idempotent_hint=True,
119+
),
120+
),
105121
ToolDefinition(
106122
description=get_prompt("semantic_layer/get_dimensions"),
107123
fn=get_dimensions,

src/dbt_mcp/semantic_layer/types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ class EntityToolResponse:
3636
description: str | None = None
3737

3838

39+
@dataclass
40+
class SavedQueryToolResponse:
41+
name: str
42+
label: str | None = None
43+
description: str | None = None
44+
metrics: list[str] | None = None
45+
group_by: list[str] | None = None
46+
where: list[str] | None = None
47+
48+
3949
@dataclass
4050
class QueryMetricsSuccess:
4151
result: str

src/dbt_mcp/tools/policy.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class ToolPolicy:
5757
ToolName.LIST_METRICS.value: ToolPolicy(
5858
name=ToolName.LIST_METRICS.value, behavior=ToolBehavior.METADATA
5959
),
60+
ToolName.LIST_SAVED_QUERIES.value: ToolPolicy(
61+
name=ToolName.LIST_SAVED_QUERIES.value, behavior=ToolBehavior.METADATA
62+
),
6063
ToolName.GET_DIMENSIONS.value: ToolPolicy(
6164
name=ToolName.GET_DIMENSIONS.value, behavior=ToolBehavior.METADATA
6265
),

src/dbt_mcp/tools/tool_names.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ToolName(Enum):
2121

2222
# Semantic Layer tools
2323
LIST_METRICS = "list_metrics"
24+
LIST_SAVED_QUERIES = "list_saved_queries"
2425
GET_DIMENSIONS = "get_dimensions"
2526
GET_ENTITIES = "get_entities"
2627
QUERY_METRICS = "query_metrics"

src/dbt_mcp/tools/toolsets.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Toolset(Enum):
2828
},
2929
Toolset.SEMANTIC_LAYER: {
3030
ToolName.LIST_METRICS,
31+
ToolName.LIST_SAVED_QUERIES,
3132
ToolName.GET_DIMENSIONS,
3233
ToolName.GET_ENTITIES,
3334
ToolName.QUERY_METRICS,

tests/unit/semantic_layer/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)