iflow-mcp_niclasolofsson-dbt-core-mcp 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. dbt_core_mcp/__init__.py +18 -0
  2. dbt_core_mcp/__main__.py +436 -0
  3. dbt_core_mcp/context.py +459 -0
  4. dbt_core_mcp/cte_generator.py +601 -0
  5. dbt_core_mcp/dbt/__init__.py +1 -0
  6. dbt_core_mcp/dbt/bridge_runner.py +1361 -0
  7. dbt_core_mcp/dbt/manifest.py +781 -0
  8. dbt_core_mcp/dbt/runner.py +67 -0
  9. dbt_core_mcp/dependencies.py +50 -0
  10. dbt_core_mcp/server.py +381 -0
  11. dbt_core_mcp/tools/__init__.py +77 -0
  12. dbt_core_mcp/tools/analyze_impact.py +78 -0
  13. dbt_core_mcp/tools/build_models.py +190 -0
  14. dbt_core_mcp/tools/demo/__init__.py +1 -0
  15. dbt_core_mcp/tools/demo/hello.html +267 -0
  16. dbt_core_mcp/tools/demo/ui_demo.py +41 -0
  17. dbt_core_mcp/tools/get_column_lineage.py +1988 -0
  18. dbt_core_mcp/tools/get_lineage.py +89 -0
  19. dbt_core_mcp/tools/get_project_info.py +96 -0
  20. dbt_core_mcp/tools/get_resource_info.py +134 -0
  21. dbt_core_mcp/tools/install_deps.py +102 -0
  22. dbt_core_mcp/tools/list_resources.py +84 -0
  23. dbt_core_mcp/tools/load_seeds.py +179 -0
  24. dbt_core_mcp/tools/query_database.py +459 -0
  25. dbt_core_mcp/tools/run_models.py +234 -0
  26. dbt_core_mcp/tools/snapshot_models.py +120 -0
  27. dbt_core_mcp/tools/test_models.py +238 -0
  28. dbt_core_mcp/utils/__init__.py +1 -0
  29. dbt_core_mcp/utils/env_detector.py +186 -0
  30. dbt_core_mcp/utils/process_check.py +130 -0
  31. dbt_core_mcp/utils/tool_utils.py +411 -0
  32. dbt_core_mcp/utils/warehouse_adapter.py +82 -0
  33. dbt_core_mcp/utils/warehouse_databricks.py +297 -0
  34. iflow_mcp_niclasolofsson_dbt_core_mcp-1.7.0.dist-info/METADATA +784 -0
  35. iflow_mcp_niclasolofsson_dbt_core_mcp-1.7.0.dist-info/RECORD +38 -0
  36. iflow_mcp_niclasolofsson_dbt_core_mcp-1.7.0.dist-info/WHEEL +4 -0
  37. iflow_mcp_niclasolofsson_dbt_core_mcp-1.7.0.dist-info/entry_points.txt +2 -0
  38. iflow_mcp_niclasolofsson_dbt_core_mcp-1.7.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,190 @@
1
+ """Run dbt build (execute models and tests together in dependency order).
2
+
3
+ This module implements the build_models tool for dbt Core MCP.
4
+ """
5
+
6
+ import logging
7
+ from typing import Any
8
+
9
+ from fastmcp.dependencies import Depends # type: ignore[reportAttributeAccessIssue]
10
+ from fastmcp.server.context import Context
11
+
12
+ from ..context import DbtCoreServerContext
13
+ from ..dependencies import get_state
14
+ from . import dbtTool
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ async def _implementation(
20
+ ctx: Context | None,
21
+ select: str | None,
22
+ exclude: str | None,
23
+ select_state_modified: bool,
24
+ select_state_modified_plus_downstream: bool,
25
+ full_refresh: bool,
26
+ resource_types: list[str] | None,
27
+ fail_fast: bool,
28
+ state: DbtCoreServerContext,
29
+ ) -> dict[str, Any]:
30
+ """Implementation function for build_models tool.
31
+
32
+ Separated for testing purposes - tests call this directly with explicit state.
33
+ The @tool() decorated build_models() function calls this with injected dependencies.
34
+ """
35
+ # Ensure dbt components are initialized
36
+ await state.ensure_initialized(ctx, force_parse=False)
37
+
38
+ # Build selector (state-based preferred when available)
39
+ selector = await state.prepare_state_based_selection(select_state_modified, select_state_modified_plus_downstream, select)
40
+
41
+ if select_state_modified and not selector:
42
+ raise RuntimeError("No previous state found - cannot determine modifications. Run 'dbt build' first to create baseline state.")
43
+
44
+ # Construct dbt CLI args for build
45
+ args = ["build"]
46
+
47
+ cache_selected_only = True
48
+ if cache_selected_only and (select or selector or select_state_modified):
49
+ args.append("--cache-selected-only")
50
+
51
+ if selector:
52
+ target_path = state.get_project_paths().get("target-path", "target")
53
+ args.extend(["-s", selector, "--state", f"{target_path}/state_last_run"])
54
+ elif select:
55
+ args.extend(["-s", select])
56
+
57
+ if exclude:
58
+ args.extend(["--exclude", exclude])
59
+
60
+ if resource_types:
61
+ for rt in resource_types:
62
+ args.extend(["--resource-type", rt])
63
+
64
+ if full_refresh:
65
+ args.append("--full-refresh")
66
+
67
+ if fail_fast:
68
+ args.append("--fail-fast")
69
+
70
+ logger.info(f"Running DBT build with args: {args}")
71
+
72
+ # Stream progress back to MCP client (if provided)
73
+ async def progress_callback(current: int, total: int, message: str) -> None:
74
+ if ctx:
75
+ await ctx.report_progress(progress=current, total=total, message=message)
76
+
77
+ # Clear stale run_results so we parse only fresh output
78
+ state.clear_stale_run_results()
79
+
80
+ runner = await state.get_runner()
81
+ result = await runner.invoke(args, progress_callback=progress_callback if ctx else None) # type: ignore
82
+
83
+ run_results = state.validate_and_parse_results(result, "build")
84
+
85
+ if result and result.success:
86
+ await state.save_execution_state()
87
+
88
+ results_list = run_results.get("results", [])
89
+ if ctx:
90
+ if results_list:
91
+ passed_count = sum(1 for r in results_list if r.get("status") in ("success", "pass"))
92
+ failed_count = sum(1 for r in results_list if r.get("status") in ("error", "fail"))
93
+ skip_count = sum(1 for r in results_list if r.get("status") == "skipped")
94
+
95
+ total = len(results_list)
96
+ parts = []
97
+ if passed_count > 0:
98
+ parts.append(f"✅ {passed_count} passed" if failed_count > 0 or skip_count > 0 else "✅ All passed")
99
+ if failed_count > 0:
100
+ parts.append(f"❌ {failed_count} failed")
101
+ if skip_count > 0:
102
+ parts.append(f"⏭️ {skip_count} skipped")
103
+
104
+ summary = f"Build: {total}/{total} resources completed ({', '.join(parts)})"
105
+ await ctx.report_progress(progress=total, total=total, message=summary)
106
+ else:
107
+ await ctx.report_progress(progress=0, total=0, message="0 resources matched selector")
108
+
109
+ if not results_list:
110
+ raise RuntimeError(f"No resources matched selector: {select or selector or 'all'}")
111
+
112
+ return {
113
+ "status": "success",
114
+ "command": " ".join(args),
115
+ "results": run_results.get("results", []),
116
+ "elapsed_time": run_results.get("elapsed_time"),
117
+ }
118
+
119
+
120
+ @dbtTool()
121
+ async def build_models(
122
+ ctx: Context,
123
+ select: str | None = None,
124
+ exclude: str | None = None,
125
+ select_state_modified: bool = False,
126
+ select_state_modified_plus_downstream: bool = False,
127
+ full_refresh: bool = False,
128
+ resource_types: list[str] | None = None,
129
+ fail_fast: bool = False,
130
+ state: DbtCoreServerContext = Depends(get_state),
131
+ ) -> dict[str, Any]:
132
+ """Run dbt build (execute models and tests together in correct dependency order).
133
+
134
+ **When to use**: This is the recommended "do everything" command that runs seeds, models,
135
+ snapshots, and tests in the correct order based on your DAG. It automatically handles
136
+ dependencies, so you don't need to run load_seeds() → run_models() → test_models() separately.
137
+
138
+ **How it works**: Executes resources in dependency order:
139
+ 1. Seeds (if selected)
140
+ 2. Models (with their upstream dependencies)
141
+ 3. Tests (after their parent models complete)
142
+ 4. Snapshots (if selected)
143
+
144
+ State-based selection modes (uses dbt state:modified selector):
145
+ - select_state_modified: Build only resources modified since last successful run (state:modified)
146
+ - select_state_modified_plus_downstream: Build modified + downstream dependencies (state:modified+)
147
+ Note: Requires select_state_modified=True
148
+
149
+ Manual selection (alternative to state-based):
150
+ - select: dbt selector syntax (e.g., "customers", "tag:mart", "stg_*")
151
+ - exclude: Exclude specific models
152
+
153
+ Args:
154
+ select: Manual selector
155
+ exclude: Exclude selector
156
+ select_state_modified: Use state:modified selector (changed resources only)
157
+ select_state_modified_plus_downstream: Extend to state:modified+ (changed + downstream)
158
+ full_refresh: Force full refresh of incremental models
159
+ resource_types: Filter by resource types (model, test, seed, snapshot)
160
+ fail_fast: Stop execution on first failure
161
+ state: Shared state object injected by FastMCP
162
+
163
+ Returns:
164
+ Build results with status, models run/tested, and timing info
165
+
166
+ See also:
167
+ - run_models(): Run only models (no tests)
168
+ - test_models(): Run only tests
169
+ - load_seeds(): Run only seeds
170
+
171
+ Examples:
172
+ # Full project build (first-time setup or comprehensive run)
173
+ build_models()
174
+
175
+ # Build only what changed (efficient incremental workflow)
176
+ build_models(select_state_modified=True)
177
+
178
+ # Build changed resources + everything downstream
179
+ build_models(select_state_modified=True, select_state_modified_plus_downstream=True)
180
+
181
+ # Build specific model and its dependencies + tests
182
+ build_models(select="customers")
183
+
184
+ # Build all marts (includes their seed dependencies automatically)
185
+ build_models(select="tag:mart")
186
+
187
+ # Quick feedback: stop on first test failure
188
+ build_models(fail_fast=True)
189
+ """
190
+ return await _implementation(ctx, select, exclude, select_state_modified, select_state_modified_plus_downstream, full_refresh, resource_types, fail_fast, state)
@@ -0,0 +1 @@
1
+ """Demo tools for MCP UI experiments."""
@@ -0,0 +1,267 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <script>
6
+ // ===== STATE =====
7
+ const state = {
8
+ nextId: 1,
9
+ hostContext: null,
10
+ toolInput: null,
11
+ lastSize: { width: 0, height: 0 }
12
+ };
13
+
14
+ // ===== LOGGING =====
15
+ const Logger = {
16
+ types: {
17
+ out: { class: 'log-out', arrow: '→', label: 'TO HOST' },
18
+ in: { class: 'log-in', arrow: '←', label: 'FROM HOST' },
19
+ system: { class: 'log-system', arrow: 'ℹ️', label: 'SYSTEM' }
20
+ },
21
+
22
+ log(type, message) {
23
+ const log = document.getElementById('log');
24
+ if (!log) return;
25
+
26
+ const config = this.types[type];
27
+ const entry = document.createElement('div');
28
+ entry.className = config.class;
29
+ entry.innerHTML = `
30
+ <div class="log-header">${new Date().toLocaleTimeString()} ${config.arrow} ${config.label}</div>
31
+ <pre>${JSON.stringify(message, null, 2)}</pre>
32
+ `;
33
+ log.appendChild(entry);
34
+ log.scrollTop = log.scrollHeight;
35
+ }
36
+ };
37
+
38
+ // ===== SIZE MANAGEMENT =====
39
+ const SizeManager = {
40
+ update() {
41
+ const viewportHeight = window.innerHeight;
42
+ const maxHeight = Math.floor(viewportHeight * 0.4);
43
+ const currentHeight = document.body.scrollHeight;
44
+
45
+ document.getElementById('viewport-height').textContent = viewportHeight;
46
+ document.getElementById('max-height').textContent = maxHeight;
47
+ document.getElementById('current-size').textContent = currentHeight;
48
+ },
49
+
50
+ notify() {
51
+ const width = document.body.scrollWidth;
52
+ const height = document.body.scrollHeight;
53
+
54
+ this.update();
55
+
56
+ if (width !== state.lastSize.width || height !== state.lastSize.height) {
57
+ state.lastSize = { width, height };
58
+ window.parent.postMessage({
59
+ jsonrpc: "2.0",
60
+ method: "ui/notifications/size-changed",
61
+ params: { width, height }
62
+ }, '*');
63
+ }
64
+ }
65
+ };
66
+
67
+ // ===== MESSAGING =====
68
+ const Messenger = {
69
+ request(method, params) {
70
+ const id = state.nextId++;
71
+ const message = { jsonrpc: "2.0", id, method, params };
72
+ Logger.log('out', message);
73
+ window.parent.postMessage(message, '*');
74
+ return id;
75
+ },
76
+
77
+ notification(method, params) {
78
+ const message = { jsonrpc: "2.0", method, params };
79
+ Logger.log('out', message);
80
+ window.parent.postMessage(message, '*');
81
+ }
82
+ };
83
+
84
+ // ===== MESSAGE HANDLERS =====
85
+ function handleResponse(message) {
86
+ if (!message.result?.hostContext) return;
87
+
88
+ state.hostContext = message.result.hostContext;
89
+
90
+ // Send initialized notification
91
+ Messenger.notification("ui/notifications/initialized", {});
92
+
93
+ // Apply theme variables from host
94
+ if (state.hostContext.styles?.variables) {
95
+ const root = document.documentElement;
96
+ Object.entries(state.hostContext.styles.variables).forEach(([key, value]) => {
97
+ root.style.setProperty(key, value);
98
+ });
99
+ console.log(`✅ Applied ${Object.keys(state.hostContext.styles.variables).length} theme variables`);
100
+
101
+ // Check if variables resolved properly before showing content
102
+ const testColor = getComputedStyle(root).getPropertyValue('--color-text-primary').trim();
103
+ if (testColor && testColor !== 'magenta') {
104
+ document.body.style.opacity = '1';
105
+ console.log('✅ Variables resolved, showing content');
106
+ } else {
107
+ // Fallback: show anyway after short delay
108
+ setTimeout(() => {
109
+ document.body.style.opacity = '1';
110
+ console.log('⚠️ Showing content with fallback colors');
111
+ }, 100);
112
+ }
113
+ }
114
+ }
115
+
116
+ function handleNotification(message) {
117
+ if (message.method === 'ui/notifications/tool-input') {
118
+ state.toolInput = message.params.arguments;
119
+ }
120
+
121
+ if (message.method === 'ui/notifications/shutdown') {
122
+ Logger.log('system', {
123
+ type: 'system',
124
+ message: 'Received shutdown notification'
125
+ });
126
+ }
127
+ }
128
+
129
+ // ===== INITIALIZATION =====
130
+ function initialize() {
131
+ console.log('🔄 Initialize called');
132
+ SizeManager.update();
133
+
134
+ Logger.log('system', {
135
+ type: 'system',
136
+ message: 'MCP App starting initialization...'
137
+ });
138
+
139
+ Messenger.request("ui/initialize", {
140
+ capabilities: {},
141
+ clientInfo: { name: "dbt-demo-ui", version: "1.0.0" },
142
+ protocolVersion: "2025-06-18"
143
+ });
144
+
145
+ // Size observer
146
+ new ResizeObserver(() => SizeManager.notify()).observe(document.body);
147
+ window.addEventListener('resize', SizeManager.update);
148
+ }
149
+
150
+ // ===== EVENT LISTENERS =====
151
+ window.addEventListener('message', event => {
152
+ const message = event.data;
153
+ Logger.log('in', message);
154
+
155
+ if (message.jsonrpc === "2.0" && message.id) {
156
+ handleResponse(message);
157
+ }
158
+
159
+ if (message.jsonrpc === "2.0" && message.method) {
160
+ handleNotification(message);
161
+ }
162
+ });
163
+
164
+ document.addEventListener('DOMContentLoaded', initialize);
165
+ </script>
166
+ <style>
167
+ * { box-sizing: border-box; }
168
+
169
+ html, body { margin: 0; padding: 0; height: auto; }
170
+
171
+ body {
172
+ padding: 0;
173
+ font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
174
+ font-size: var(--font-text-md-size, 13px);
175
+ color: var(--color-text-primary, magenta);
176
+ background: transparent;
177
+ opacity: 0;
178
+ transition: opacity 0.15s ease-in;
179
+ }
180
+
181
+ h1 {
182
+ margin: 0 0 8px;
183
+ font-size: var(--font-heading-md-size, 20px);
184
+ font-weight: var(--font-weight-semibold, 600);
185
+ color: var(--color-text-primary, magenta);
186
+ }
187
+
188
+ p {
189
+ margin: 0 0 16px;
190
+ color: var(--color-text-secondary, yellow);
191
+ }
192
+
193
+ #size-info {
194
+ background: var(--color-background-secondary, #ff1493);
195
+ border: var(--border-width-regular, 1px) solid var(--color-border-primary, red);
196
+ border-radius: var(--border-radius-lg, 6px);
197
+ padding: 12px;
198
+ margin-bottom: 16px;
199
+ font-family: var(--font-mono, monospace);
200
+ font-size: var(--font-text-sm-size, 11px);
201
+ }
202
+
203
+ #size-info div { margin: 4px 0; }
204
+ #size-info strong { color: var(--color-text-primary, magenta); }
205
+ #size-info span { color: var(--color-text-success, lime); }
206
+
207
+ #log {
208
+ background: var(--color-background-secondary, #ff1493);
209
+ border: var(--border-width-regular, 1px) solid var(--color-border-primary, red);
210
+ border-radius: var(--border-radius-lg, 6px);
211
+ padding: 12px;
212
+ }
213
+
214
+ .log-out, .log-in, .log-system {
215
+ margin-bottom: 12px;
216
+ padding: 8px;
217
+ border-radius: var(--border-radius-md, 4px);
218
+ }
219
+
220
+ .log-out {
221
+ background: var(--color-background-info, #8b00ff);
222
+ border-left: 3px solid var(--color-border-info, cyan);
223
+ }
224
+
225
+ .log-in {
226
+ background: rgba(115, 201, 145, 0.1);
227
+ border-left: 3px solid var(--color-border-success, lime);
228
+ }
229
+
230
+ .log-system {
231
+ background: rgba(128, 128, 128, 0.1);
232
+ border-left: 3px solid var(--color-border-primary, red);
233
+ }
234
+
235
+ .log-header {
236
+ font-size: var(--font-text-xs-size, 10px);
237
+ font-weight: var(--font-weight-semibold, 600);
238
+ margin-bottom: 4px;
239
+ color: var(--color-text-secondary, yellow);
240
+ }
241
+
242
+ .log-out .log-header { color: var(--color-text-primary, magenta); }
243
+ .log-in .log-header { color: var(--color-text-success, lime); }
244
+
245
+ pre {
246
+ margin: 0;
247
+ font-family: var(--font-mono, 'Consolas', 'Monaco', monospace);
248
+ font-size: var(--font-text-xs-size, 10px);
249
+ color: var(--color-text-primary, magenta);
250
+ white-space: pre-wrap;
251
+ word-wrap: break-word;
252
+ }
253
+ </style>
254
+ </head>
255
+ <body>
256
+ <h1>MCP Apps Protocol Monitor 📡</h1>
257
+ <p>Self-logging JSON-RPC message viewer</p>
258
+
259
+ <div id="size-info">
260
+ <div><strong>Viewport Height:</strong> <span id="viewport-height">-</span>px</div>
261
+ <div><strong>40% Max (40vh):</strong> <span id="max-height">-</span>px</div>
262
+ <div><strong>Current Body Size:</strong> <span id="current-size">-</span>px</div>
263
+ </div>
264
+
265
+ <div id="log"></div>
266
+ </body>
267
+ </html>
@@ -0,0 +1,41 @@
1
+ """Simple tool for MCP UI resource testing."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from fastmcp.server.context import Context
7
+
8
+ from .. import dbtTool
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dbtTool(
14
+ description="Render the demo UI resource (resource://demo/hello).",
15
+ meta={
16
+ "ui": {
17
+ "resourceUri": "resource://demo/hello",
18
+ }
19
+ },
20
+ )
21
+ async def demo_ui(_ctx: Context) -> dict[str, Any]:
22
+ """Render the demo UI resource (resource://demo/hello)."""
23
+ logger.info("demo_ui tool called")
24
+ return {
25
+ "message": "Demo UI tool invoked",
26
+ }
27
+
28
+
29
+ # @dbtTool(description="Return basic client context metadata for diagnostics.")
30
+ # async def demo_client_context(ctx: Context) -> dict[str, Any]:
31
+ # """Return basic client context metadata for diagnostics."""
32
+ # logger.info("demo_client_context tool called")
33
+ # meta = getattr(getattr(ctx, "request_context", None), "meta", None)
34
+ # meta_value = meta if meta is None else getattr(meta, "__dict__", meta)
35
+ # return {
36
+ # "client_id": getattr(ctx, "client_id", None),
37
+ # "request_id": getattr(ctx, "request_id", None),
38
+ # "session_id": getattr(ctx, "session_id", None),
39
+ # "has_sampling_capability": getattr(ctx, "has_sampling_capability", None),
40
+ # "request_meta": meta_value,
41
+ # }