Skip to content

Commit bf53d4d

Browse files
feat: add get_lineage direction param; mark deprecated tools in README; regenerate contract snapshot
1 parent 0739d2c commit bf53d4d

15 files changed

Lines changed: 329 additions & 60 deletions
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
kind: Enhancement or New Feature
2-
body: Enrich get_lineage results with a description field per node; deprecate get_model_parents and get_model_children in favor of get_lineage(depth=1)
2+
body: Enrich get_lineage results with a description field per node; add a direction param (upstream/downstream/both) to narrow results to one side of the graph; deprecate get_model_parents and get_model_children in favor of get_lineage(depth=1, direction=...)
33
time: 2026-06-29T11:30:27.420545-07:00

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ To learn more about the dbt Discovery API, click [here](https://docs.getdbt.com/
5050
- `get_lineage`: Gets full lineage graph (ancestors and descendants) with type and depth filtering.
5151
- `get_macro_details`: Gets details for a specific macro.
5252
- `get_mart_models`: Retrieves all mart models.
53-
- `get_model_children`: Gets downstream dependents of a model.
53+
- `get_model_children`: Gets downstream dependents of a model. **Deprecated** — use `get_lineage` instead.
5454
- `get_model_details`: Gets model details including compiled SQL, columns, and schema.
5555
- `get_model_health`: Gets health signals: run status, test results, and upstream source freshness.
56-
- `get_model_parents`: Gets upstream dependencies of a model.
56+
- `get_model_parents`: Gets upstream dependencies of a model. **Deprecated** — use `get_lineage` instead.
5757
- `get_model_performance`: Gets execution history for a model; option to include test results.
5858
- `get_related_models`: Finds similar models using semantic search.
5959
- `get_seed_details`: Gets details for a specific seed.

scripts/generate_docs.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
import sys
55
from pathlib import Path
66

7-
from dbt_mcp.tools.readme_mappings import HUMAN_DESCRIPTIONS, TOOLSET_DESCRIPTIONS
7+
from dbt_mcp.tools.readme_mappings import (
8+
DEPRECATED_TOOLS,
9+
HUMAN_DESCRIPTIONS,
10+
TOOLSET_DESCRIPTIONS,
11+
)
812
from dbt_mcp.tools.toolsets import toolsets
913

1014
logging.basicConfig(level=logging.INFO, format="%(message)s")
@@ -50,7 +54,11 @@ def generate_readme_tools_section() -> str:
5054
sorted_tools = sorted(tool_names, key=lambda t: t.value)
5155
for tool in sorted_tools:
5256
desc = HUMAN_DESCRIPTIONS.get(tool, "")
53-
lines.append(f"- `{tool.value}`: {desc}")
57+
line = f"- `{tool.value}`: {desc}"
58+
replacement = DEPRECATED_TOOLS.get(tool)
59+
if replacement:
60+
line += f" **Deprecated** — use `{replacement}` instead."
61+
lines.append(line)
5462

5563
lines.append("") # Empty line after each section
5664

src/dbt_mcp/discovery/client.py

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from dbt_mcp.errors import InvalidParameterError, ToolCallError
1313
from dbt_mcp.errors.common import NotFoundError
1414
from dbt_mcp.gql.errors import raise_gql_error
15-
from dbt_mcp.tools.parameters import LineageResourceType
15+
from dbt_mcp.tools.parameters import LineageDirection, LineageResourceType
1616

1717
DEFAULT_PAGE_SIZE = 100
1818
DEFAULT_MAX_NODE_QUERY_LIMIT = 10000
@@ -819,6 +819,7 @@ async def fetch_lineage(
819819
unique_id: str,
820820
depth: int,
821821
types: list[LineageResourceType] | None = None,
822+
direction: LineageDirection = LineageDirection.BOTH,
822823
*,
823824
config: DiscoveryConfig,
824825
) -> list[dict]:
@@ -828,10 +829,11 @@ async def fetch_lineage(
828829
unique_id: The dbt unique ID of the resource to get lineage for.
829830
depth: how many levels to traverse (0 = infinite, 1 = immediate neighbors only, higher = deeper)
830831
types: List of resource types to include. If None, includes all types.
832+
direction: Which direction(s) to traverse relative to unique_id.
831833
config: Discovery API connection and environment.
832834
833835
Returns:
834-
List of nodes connected to unique_id (upstream + downstream).
836+
List of nodes connected to unique_id, filtered to `direction`.
835837
"""
836838
if depth < 0:
837839
raise ToolCallError("Depth must be greater than or equal to 0")
@@ -857,17 +859,22 @@ async def fetch_lineage(
857859
)
858860

859861
# Filter to connected nodes only
860-
return self._filter_connected_nodes(all_nodes, unique_id, depth)
862+
return self._filter_connected_nodes(all_nodes, unique_id, depth, direction)
861863

862864
def _filter_connected_nodes(
863-
self, nodes: list[dict], target_id: str, depth: int
865+
self,
866+
nodes: list[dict],
867+
target_id: str,
868+
depth: int,
869+
direction: LineageDirection = LineageDirection.BOTH,
864870
) -> list[dict]:
865-
"""Return only nodes connected to target_id (upstream and downstream).
866-
Uses BFS to find all nodes reachable from target in both directions.
871+
"""Return only nodes connected to target_id, filtered to `direction`.
872+
Uses BFS to find all nodes reachable from target in the requested direction(s).
867873
Args:
868874
nodes: List of all nodes in the lineage graph.
869875
target_id: The unique ID of the target node.
870876
depth: how many levels to traverse (0 = infinite, 1 = immediate neighbors only, higher = deeper)
877+
direction: Which direction(s) to traverse relative to target_id.
871878
"""
872879
node_map = {
873880
n["uniqueId"]: n
@@ -882,6 +889,15 @@ def _filter_connected_nodes(
882889
if target_id not in node_map:
883890
return []
884891

892+
include_upstream = direction in (
893+
LineageDirection.UPSTREAM,
894+
LineageDirection.BOTH,
895+
)
896+
include_downstream = direction in (
897+
LineageDirection.DOWNSTREAM,
898+
LineageDirection.BOTH,
899+
)
900+
885901
# BFS to find all connected nodes
886902
connected = {target_id}
887903
queue = [(target_id, 0)]
@@ -897,22 +913,24 @@ def _filter_connected_nodes(
897913
continue
898914

899915
# Traverse upstream (parents)
900-
for parent_id in node.get("parentIds", []):
901-
if parent_id not in connected and parent_id in node_map:
902-
connected.add(parent_id)
903-
queue.append((parent_id, current_depth + 1))
916+
if include_upstream:
917+
for parent_id in node.get("parentIds", []):
918+
if parent_id not in connected and parent_id in node_map:
919+
connected.add(parent_id)
920+
queue.append((parent_id, current_depth + 1))
904921

905922
# Traverse downstream (children)
906-
for candidate in nodes:
907-
candidate_id = candidate.get("uniqueId")
908-
if not candidate_id or candidate_id not in node_map:
909-
continue
910-
if (
911-
current_id in candidate.get("parentIds", [])
912-
and candidate_id not in connected
913-
):
914-
connected.add(candidate_id)
915-
queue.append((candidate_id, current_depth + 1))
923+
if include_downstream:
924+
for candidate in nodes:
925+
candidate_id = candidate.get("uniqueId")
926+
if not candidate_id or candidate_id not in node_map:
927+
continue
928+
if (
929+
current_id in candidate.get("parentIds", [])
930+
and candidate_id not in connected
931+
):
932+
connected.add(candidate_id)
933+
queue.append((candidate_id, current_depth + 1))
916934

917935
# Return in original order
918936
return [node_map[uid] for uid in connected]

src/dbt_mcp/discovery/param_descriptions.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@
3636

3737
# Not a JSON Schema param description — used as the `arg_mapping` for the
3838
# get_model_parents/get_model_children deprecation banner. get_lineage isn't a
39-
# drop-in: it requires unique_id (these tools accept name alone), defaults to
40-
# depth=5, and even at depth=1 returns the target node plus BOTH parents and
41-
# children (LineageFetcher._filter_connected_nodes traverses parentIds in both
42-
# directions) — callers must filter the result themselves to isolate one
43-
# direction, using the parentIds field each node already carries.
44-
GET_LINEAGE_ARG_MAPPING = (
45-
"Call get_lineage(unique_id=..., depth=1) — it requires unique_id (not name) "
46-
"and, at depth=1, returns the target node plus both parents and children "
47-
"(not filtered to one direction). Derive one direction from the result via "
48-
"parentIds: parents are nodes whose uniqueId is in the target's parentIds; "
49-
"children are nodes whose parentIds contain the target's uniqueId."
39+
# drop-in on its own: it requires unique_id (these tools accept name alone) and
40+
# defaults to depth=5. The `direction` param (upstream/downstream/both) makes it
41+
# a true drop-in once both differences are called out.
42+
GET_MODEL_PARENTS_ARG_MAPPING = (
43+
'Call get_lineage(unique_id=..., depth=1, direction="upstream") — it '
44+
'requires unique_id (not name) and defaults to depth=5; direction="upstream" '
45+
"returns only parents, matching this tool's behavior."
46+
)
47+
48+
GET_MODEL_CHILDREN_ARG_MAPPING = (
49+
'Call get_lineage(unique_id=..., depth=1, direction="downstream") — it '
50+
'requires unique_id (not name) and defaults to depth=5; direction="downstream" '
51+
"returns only children, matching this tool's behavior."
5052
)

src/dbt_mcp/discovery/tools.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
SourcesFetcher,
1919
)
2020
from dbt_mcp.discovery.param_descriptions import (
21-
GET_LINEAGE_ARG_MAPPING,
21+
GET_MODEL_CHILDREN_ARG_MAPPING,
22+
GET_MODEL_PARENTS_ARG_MAPPING,
2223
MACRO_INCLUDE_DEFAULT_DBT_PACKAGES,
2324
MACRO_PACKAGE_NAMES,
2425
MACRO_RETURN_PACKAGE_NAMES_ONLY,
@@ -32,12 +33,13 @@
3233
from dbt_mcp.tools.deprecation import deprecated_description, deprecation_meta
3334
from dbt_mcp.tools.fields import (
3435
DEPTH_FIELD,
36+
DIRECTION_FIELD,
3537
NAME_FIELD,
3638
TYPES_FIELD,
3739
UNIQUE_ID_FIELD,
3840
UNIQUE_ID_REQUIRED_FIELD,
3941
)
40-
from dbt_mcp.tools.parameters import LineageResourceType
42+
from dbt_mcp.tools.parameters import LineageDirection, LineageResourceType
4143
from dbt_mcp.tools.register import register_tools
4244
from dbt_mcp.tools.tool_names import ToolName
4345
from dbt_mcp.tools.toolsets import Toolset
@@ -163,7 +165,7 @@ async def get_model_details(
163165

164166
@dbt_mcp_tool(
165167
description=deprecated_description(
166-
replacement="get_lineage", arg_mapping=GET_LINEAGE_ARG_MAPPING
168+
replacement="get_lineage", arg_mapping=GET_MODEL_PARENTS_ARG_MAPPING
167169
),
168170
meta=deprecation_meta(replacement="get_lineage"),
169171
title="Get Model Parents",
@@ -184,7 +186,7 @@ async def get_model_parents(
184186

185187
@dbt_mcp_tool(
186188
description=deprecated_description(
187-
replacement="get_lineage", arg_mapping=GET_LINEAGE_ARG_MAPPING
189+
replacement="get_lineage", arg_mapping=GET_MODEL_CHILDREN_ARG_MAPPING
188190
),
189191
meta=deprecation_meta(replacement="get_lineage"),
190192
title="Get Model Children",
@@ -266,10 +268,15 @@ async def get_lineage(
266268
unique_id: str = UNIQUE_ID_REQUIRED_FIELD,
267269
types: list[LineageResourceType] | None = TYPES_FIELD,
268270
depth: int = DEPTH_FIELD,
271+
direction: LineageDirection = DIRECTION_FIELD,
269272
) -> list[dict]:
270273
config = await context.config_provider.get_config()
271274
return await context.lineage_fetcher.fetch_lineage(
272-
unique_id=unique_id, types=types, depth=depth, config=config
275+
unique_id=unique_id,
276+
types=types,
277+
depth=depth,
278+
direction=direction,
279+
config=config,
273280
)
274281

275282

src/dbt_mcp/discovery/tools_multiproject.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
)
2424
from dbt_mcp.discovery.param_descriptions import (
2525
DISCOVERY_PROJECT_ID_DESCRIPTION,
26-
GET_LINEAGE_ARG_MAPPING,
26+
GET_MODEL_CHILDREN_ARG_MAPPING,
27+
GET_MODEL_PARENTS_ARG_MAPPING,
2728
MACRO_INCLUDE_DEFAULT_DBT_PACKAGES,
2829
MACRO_PACKAGE_NAMES,
2930
MACRO_RETURN_PACKAGE_NAMES_ONLY,
@@ -37,12 +38,13 @@
3738
from dbt_mcp.tools.deprecation import deprecated_description, deprecation_meta
3839
from dbt_mcp.tools.fields import (
3940
DEPTH_FIELD,
41+
DIRECTION_FIELD,
4042
NAME_FIELD,
4143
TYPES_FIELD,
4244
UNIQUE_ID_FIELD,
4345
UNIQUE_ID_REQUIRED_FIELD,
4446
)
45-
from dbt_mcp.tools.parameters import LineageResourceType
47+
from dbt_mcp.tools.parameters import LineageDirection, LineageResourceType
4648
from dbt_mcp.tools.register import register_tools
4749
from dbt_mcp.tools.tool_names import ToolName
4850
from dbt_mcp.tools.toolsets import Toolset
@@ -175,7 +177,7 @@ async def get_model_details(
175177

176178
@dbt_mcp_tool(
177179
description=deprecated_description(
178-
replacement="get_lineage", arg_mapping=GET_LINEAGE_ARG_MAPPING
180+
replacement="get_lineage", arg_mapping=GET_MODEL_PARENTS_ARG_MAPPING
179181
),
180182
meta=deprecation_meta(replacement="get_lineage"),
181183
title="Get Model Parents",
@@ -197,7 +199,7 @@ async def get_model_parents(
197199

198200
@dbt_mcp_tool(
199201
description=deprecated_description(
200-
replacement="get_lineage", arg_mapping=GET_LINEAGE_ARG_MAPPING
202+
replacement="get_lineage", arg_mapping=GET_MODEL_CHILDREN_ARG_MAPPING
201203
),
202204
meta=deprecation_meta(replacement="get_lineage"),
203205
title="Get Model Children",
@@ -284,10 +286,15 @@ async def get_lineage(
284286
unique_id: str = UNIQUE_ID_REQUIRED_FIELD,
285287
types: list[LineageResourceType] | None = TYPES_FIELD,
286288
depth: int = DEPTH_FIELD,
289+
direction: LineageDirection = DIRECTION_FIELD,
287290
) -> list[dict]:
288291
config = await context.config_provider.get_config(project_id=project_id)
289292
return await context.lineage_fetcher.fetch_lineage(
290-
unique_id=unique_id, types=types, depth=depth, config=config
293+
unique_id=unique_id,
294+
types=types,
295+
depth=depth,
296+
direction=direction,
297+
config=config,
291298
)
292299

293300

src/dbt_mcp/prompts/discovery/get_lineage.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ A list of all nodes in the connected subgraph, where each node contains:
1010
- `description`: The resource's description, if one is defined (may be null)
1111
- `parentIds`: List of unique IDs that this resource directly depends on
1212

13-
Call `get_lineage(unique_id=..., depth=1)` to retrieve only the immediate parents and children of a resource.
13+
Call `get_lineage(unique_id=..., depth=1)` to retrieve the target node plus only its immediate parents and children.
14+
15+
Use `direction` to narrow the response to one side of the graph and reduce payload size:
16+
- `direction="upstream"`: target node plus ancestors only (no descendants)
17+
- `direction="downstream"`: target node plus descendants only (no ancestors)
18+
- `direction="both"` (default): target node plus both ancestors and descendants
1419

1520
**Example Response:**
1621
```json
@@ -52,6 +57,12 @@ get_lineage(unique_id="model.analytics.customers", depth=1)
5257

5358
# Get deeper lineage for comprehensive analysis
5459
get_lineage(unique_id="model.analytics.customers", depth=10)
60+
61+
# Get only upstream dependencies (ancestors), e.g. for dependency tracking
62+
get_lineage(unique_id="model.analytics.customers", direction="upstream")
63+
64+
# Get only downstream dependents (descendants), e.g. for impact analysis
65+
get_lineage(unique_id="model.analytics.customers", direction="downstream")
5566
```
5667

5768
**Traversing the Graph:**

src/dbt_mcp/tools/fields.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from pydantic import Field
44

5+
from dbt_mcp.tools.parameters import LineageDirection
6+
57

68
_UNIQUE_ID_DESCRIPTION = (
79
"Fully-qualified unique ID of the resource. "
@@ -40,3 +42,10 @@
4042
"If not provided, includes all types. "
4143
"Valid types: Model, Source, Seed, Snapshot, Exposure, Metric, SemanticModel, SavedQuery, Test.",
4244
)
45+
46+
DIRECTION_FIELD = Field(
47+
default=LineageDirection.BOTH,
48+
description="Which direction(s) of the graph to return, relative to the target node: "
49+
"`upstream` (ancestors/parents only), `downstream` (descendants/children only), "
50+
"or `both` (default). Narrowing to one direction reduces response size.",
51+
)

src/dbt_mcp/tools/parameters.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ class LineageResourceType(StrEnum):
1515
SEMANTIC_MODEL = "SemanticModel"
1616
SAVED_QUERY = "SavedQuery"
1717
TEST = "Test"
18+
19+
20+
class LineageDirection(StrEnum):
21+
"""Which direction(s) of the lineage graph to include, relative to the target."""
22+
23+
UPSTREAM = "upstream"
24+
DOWNSTREAM = "downstream"
25+
BOTH = "both"

0 commit comments

Comments
 (0)