datasecops-cli 0.5.1__tar.gz → 0.5.3__tar.gz

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 (57) hide show
  1. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/CHANGELOG.md +29 -0
  2. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/PKG-INFO +54 -4
  3. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/README.md +53 -3
  4. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/docs/getting-started.md +16 -13
  5. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/docs/mcp-server.md +12 -2
  6. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/mcp-servers.json +9 -1
  7. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/pyproject.toml +1 -1
  8. datasecops_cli-0.5.3/src/datasecops_cli/__init__.py +1 -0
  9. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/main.py +40 -43
  10. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/menus/configuration.py +11 -57
  11. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/menus/development.py +24 -3
  12. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/menus/downloads.py +11 -57
  13. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/linting_service.py +29 -1
  14. datasecops_cli-0.5.3/src/datasecops_cli/utilities/mcp.py +74 -0
  15. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_mcp/connection.py +8 -0
  16. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_mcp/server.py +129 -8
  17. datasecops_cli-0.5.1/src/datasecops_cli/__init__.py +0 -1
  18. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/.github/workflows/auto-tag.yml +0 -0
  19. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/.github/workflows/publish-cli.yml +0 -0
  20. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/.github/workflows/test.yml +0 -0
  21. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/.gitignore +0 -0
  22. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/DEVELOPMENT.md +0 -0
  23. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/LICENSE +0 -0
  24. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/docs/git-operations.md +0 -0
  25. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/docs/legacy.md +0 -0
  26. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/docs/legacy_plan_of_action.md +0 -0
  27. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/setup.ps1 +0 -0
  28. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/setup.sh +0 -0
  29. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/config.py +0 -0
  30. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/menus/__init__.py +0 -0
  31. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/menus/git_operations.py +0 -0
  32. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/models/__init__.py +0 -0
  33. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/models/git_helpers.py +0 -0
  34. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/models/project_config.py +0 -0
  35. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/__init__.py +0 -0
  36. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/bootstrap_service.py +0 -0
  37. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
  38. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/dbt_runner.py +0 -0
  39. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
  40. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/download_service.py +0 -0
  41. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/git_service.py +0 -0
  42. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/skill_service.py +0 -0
  43. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/snowflake_service.py +0 -0
  44. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/services/upstream_service.py +0 -0
  45. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/utilities/__init__.py +0 -0
  46. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/utilities/display.py +0 -0
  47. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/utilities/file_utils.py +0 -0
  48. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
  49. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_mcp/__init__.py +0 -0
  50. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/src/datasecops_mcp/__main__.py +0 -0
  51. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/tests/__init__.py +0 -0
  52. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/tests/test_config.py +0 -0
  53. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/tests/test_file_utils.py +0 -0
  54. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/tests/test_main.py +0 -0
  55. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/tests/test_models.py +0 -0
  56. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/tests/test_version.py +0 -0
  57. {datasecops_cli-0.5.1 → datasecops_cli-0.5.3}/tests/test_yaml_utils.py +0 -0
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to the DataSecOps CLI are documented in this file.
4
4
 
5
+ ## [0.5.3] - 2026-06-05
6
+
7
+ ### Changed
8
+
9
+ - **MCP linting tools report progress** — `lint_sql`, `fix_sql`, and `lint_project` now use MCP `Context` to send progress notifications and info messages during execution, so callers can see what stage the tool is at (project discovery, running SQLFluff, processing results).
10
+ - **MCP linting tools no longer block the event loop** — subprocess calls to SQLFluff are wrapped in `asyncio.to_thread` so the MCP server remains responsive while linting runs.
11
+ - **`_ensure_dbt_packages` skips `dbt deps` when packages exist** — `dbt deps` is only triggered when `dbt_packages/` is actually empty, but the `dbt-snowflake` Python package check always runs to ensure the dbt templater works.
12
+
13
+ ## [0.5.2] - 2026-06-05
14
+
15
+ ### Added
16
+
17
+ - **`get_sqlfluff_config` MCP tool** — exposes the rendered `.sqlfluff` INI config file from the native app, allowing AI tools to retrieve the exact linting configuration without needing a local `.sqlfluff` file.
18
+ - **`get_documentation` MCP tool** — exposes framework documentation topics (model development, data governance, deployment, etc.) so AI tools can reference framework conventions directly.
19
+
20
+ ### Changed
21
+
22
+ - **Cortex Code MCP config is now project-level** — MCP server registration for Cortex Code now writes `.snowflake/cortex/mcp.json` in the project root instead of calling `cortex mcp add`. This enables project-scoped MCP servers in Cortex Code Desktop and removes the dependency on the `cortex` binary being installed.
23
+ - **All MCP servers included in Cortex Code config** — the datasecops-framework, GitHub, Azure DevOps, and dbt MCP servers are all written to the project-level config file with `"type": "stdio"` field.
24
+ - **Cortex Code always available as an AI tool option** — removed the `shutil.which("cortex")` check since we no longer need the cortex binary for MCP configuration.
25
+ - **Expanded `_ALLOWED_PROCEDURES`** — added `get_sqlfluff_config_file` and `get_documentation` to the MCP connection allowlist.
26
+
27
+ ### Fixed
28
+
29
+ - **Linting now installs dbt-snowflake at framework-pinned version** — the CLI lint menu fetches both SQLFluff and dbt requirements from the native app and ensures all packages (including `dbt-core` and `dbt-snowflake`) are installed at the correct versions before linting.
30
+ - **Linting runs `dbt deps` if packages are missing** — if `packages.yml` exists but `dbt_packages/` is empty, `dbt deps` is run automatically before linting to satisfy the sqlfluff dbt templater.
31
+ - **Default `.sqlfluffignore` created on first lint** — excludes `target/`, `dbt_packages/`, `dbt_modules/`, `logs/`, `macros/`, `models/cortex/agents`, `models/cortex/semantic_views`, `objects/stages/config/`, and `objects/file_formats/config/`.
32
+ - **MCP linting tools check for dbt-snowflake** — the MCP server lint/fix tools now verify `dbt-snowflake` is installed (and install it if missing) before running SQLFluff.
33
+
5
34
  ## [0.5.1] - 2026-06-04
6
35
 
7
36
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasecops-cli
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: DataSecOps Framework CLI for Snowflake Native App
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -158,10 +158,60 @@ The package includes an MCP (Model Context Protocol) server that exposes your fr
158
158
 
159
159
  ### Setup for AI Tools
160
160
 
161
- **Cortex Code:**
161
+ **Cortex Code** — add to `.snowflake/cortex/mcp.json` in your project root:
162
162
 
163
- ```bash
164
- cortex mcp add datasecops-framework -- datasecops-mcp --connection-name <CONNECTION> --app-database <APP_DB>
163
+ ```json
164
+ {
165
+ "mcpServers": {
166
+ "datasecops-framework": {
167
+ "type": "stdio",
168
+ "command": "datasecops-mcp",
169
+ "args": ["--connection-name", "<CONNECTION>", "--app-database", "<APP_DB>"]
170
+ },
171
+ "github": {
172
+ "type": "stdio",
173
+ "command": "npx",
174
+ "args": ["-y", "@modelcontextprotocol/server-github"],
175
+ "env": {
176
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "<your-github-pat>"
177
+ }
178
+ },
179
+ "dbt": {
180
+ "type": "stdio",
181
+ "command": "uvx",
182
+ "args": ["dbt-mcp"]
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ For Azure DevOps instead of GitHub:
189
+
190
+ ```json
191
+ {
192
+ "mcpServers": {
193
+ "datasecops-framework": {
194
+ "type": "stdio",
195
+ "command": "datasecops-mcp",
196
+ "args": ["--connection-name", "<CONNECTION>", "--app-database", "<APP_DB>"]
197
+ },
198
+ "azure-devops": {
199
+ "type": "stdio",
200
+ "command": "npx",
201
+ "args": ["-y", "@tiberriver256/mcp-server-azure-devops"],
202
+ "env": {
203
+ "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/<your-org>",
204
+ "AZURE_DEVOPS_AUTH_METHOD": "pat",
205
+ "AZURE_DEVOPS_PAT": "<your-azure-pat>"
206
+ }
207
+ },
208
+ "dbt": {
209
+ "type": "stdio",
210
+ "command": "uvx",
211
+ "args": ["dbt-mcp"]
212
+ }
213
+ }
214
+ }
165
215
  ```
166
216
 
167
217
  **VS Code / Cursor** — add to `.vscode/mcp.json` or `.cursor/mcp.json`:
@@ -139,10 +139,60 @@ The package includes an MCP (Model Context Protocol) server that exposes your fr
139
139
 
140
140
  ### Setup for AI Tools
141
141
 
142
- **Cortex Code:**
142
+ **Cortex Code** — add to `.snowflake/cortex/mcp.json` in your project root:
143
143
 
144
- ```bash
145
- cortex mcp add datasecops-framework -- datasecops-mcp --connection-name <CONNECTION> --app-database <APP_DB>
144
+ ```json
145
+ {
146
+ "mcpServers": {
147
+ "datasecops-framework": {
148
+ "type": "stdio",
149
+ "command": "datasecops-mcp",
150
+ "args": ["--connection-name", "<CONNECTION>", "--app-database", "<APP_DB>"]
151
+ },
152
+ "github": {
153
+ "type": "stdio",
154
+ "command": "npx",
155
+ "args": ["-y", "@modelcontextprotocol/server-github"],
156
+ "env": {
157
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "<your-github-pat>"
158
+ }
159
+ },
160
+ "dbt": {
161
+ "type": "stdio",
162
+ "command": "uvx",
163
+ "args": ["dbt-mcp"]
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ For Azure DevOps instead of GitHub:
170
+
171
+ ```json
172
+ {
173
+ "mcpServers": {
174
+ "datasecops-framework": {
175
+ "type": "stdio",
176
+ "command": "datasecops-mcp",
177
+ "args": ["--connection-name", "<CONNECTION>", "--app-database", "<APP_DB>"]
178
+ },
179
+ "azure-devops": {
180
+ "type": "stdio",
181
+ "command": "npx",
182
+ "args": ["-y", "@tiberriver256/mcp-server-azure-devops"],
183
+ "env": {
184
+ "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/<your-org>",
185
+ "AZURE_DEVOPS_AUTH_METHOD": "pat",
186
+ "AZURE_DEVOPS_PAT": "<your-azure-pat>"
187
+ }
188
+ },
189
+ "dbt": {
190
+ "type": "stdio",
191
+ "command": "uvx",
192
+ "args": ["dbt-mcp"]
193
+ }
194
+ }
195
+ }
146
196
  ```
147
197
 
148
198
  **VS Code / Cursor** — add to `.vscode/mcp.json` or `.cursor/mcp.json`:
@@ -215,20 +215,23 @@ Create `.cursor/mcp.json` in your project root with the same format as above.
215
215
 
216
216
  ### Cortex Code (CoCo)
217
217
 
218
- Add servers from the command line:
218
+ Create `.snowflake/cortex/mcp.json` in your project root:
219
219
 
220
- ```bash
221
- # DataSecOps Framework
222
- cortex mcp add datasecops-framework -- datasecops-mcp --connection-name <CONNECTION> --app-database <APP_DB>
223
-
224
- # dbt
225
- cortex mcp add dbt -- uvx dbt-mcp
226
-
227
- # GitHub
228
- cortex mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN=$GITHUB_TOKEN -- npx -y @modelcontextprotocol/server-github
229
-
230
- # OR Azure DevOps
231
- cortex mcp add azure-devops -e AZURE_DEVOPS_ORG_URL=$AZURE_DEVOPS_ORG_URL -e AZURE_DEVOPS_AUTH_METHOD=pat -e AZURE_DEVOPS_PAT=$AZURE_DEVOPS_PAT -- npx -y @tiberriver256/mcp-server-azure-devops
220
+ ```json
221
+ {
222
+ "mcpServers": {
223
+ "datasecops-framework": {
224
+ "type": "stdio",
225
+ "command": "datasecops-mcp",
226
+ "args": ["--connection-name", "<CONNECTION>", "--app-database", "<APP_DB>"]
227
+ },
228
+ "dbt": {
229
+ "type": "stdio",
230
+ "command": "uvx",
231
+ "args": ["dbt-mcp"]
232
+ }
233
+ }
234
+ }
232
235
  ```
233
236
 
234
237
  Verify all servers are connected:
@@ -24,8 +24,18 @@ If `--connection-name` and `--app-database` are not provided, the server falls b
24
24
 
25
25
  ### Cortex Code
26
26
 
27
- ```bash
28
- cortex mcp add datasecops-framework -- datasecops-mcp --connection-name <CONNECTION> --app-database <APP_DB>
27
+ Create `.snowflake/cortex/mcp.json` in your project root:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "datasecops-framework": {
33
+ "type": "stdio",
34
+ "command": "datasecops-mcp",
35
+ "args": ["--connection-name", "<CONNECTION>", "--app-database", "<APP_DB>"]
36
+ }
37
+ }
38
+ }
29
39
  ```
30
40
 
31
41
  ### Claude Code
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "datasecops-framework": {
4
+ "type": "stdio",
4
5
  "command": "datasecops-mcp",
5
6
  "args": ["--connection-name", "<CONNECTION>", "--app-database", "<APP_DB>"],
6
- "env": {},
7
7
  "description": "DataSecOps Framework governance rules, branching conventions, linting config, and project profiles from your Snowflake Native App"
8
8
  },
9
9
  "github": {
10
+ "type": "stdio",
10
11
  "command": "npx",
11
12
  "args": ["-y", "@modelcontextprotocol/server-github"],
12
13
  "env": {
@@ -15,6 +16,7 @@
15
16
  "description": "GitHub API access for PRs, issues, checks, and repository management"
16
17
  },
17
18
  "azure-devops": {
19
+ "type": "stdio",
18
20
  "command": "npx",
19
21
  "args": ["-y", "@tiberriver256/mcp-server-azure-devops"],
20
22
  "env": {
@@ -23,6 +25,12 @@
23
25
  "AZURE_DEVOPS_PAT": "<your-azure-pat>"
24
26
  },
25
27
  "description": "Azure DevOps API access for PRs, pipelines, work items, and boards"
28
+ },
29
+ "dbt": {
30
+ "type": "stdio",
31
+ "command": "uvx",
32
+ "args": ["dbt-mcp"],
33
+ "description": "dbt lineage, model discovery, codegen, and semantic layer"
26
34
  }
27
35
  }
28
36
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datasecops-cli"
7
- version = "0.5.1"
7
+ version = "0.5.3"
8
8
  description = "DataSecOps Framework CLI for Snowflake Native App"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.5.3"
@@ -307,66 +307,63 @@ def _run_setup(project_dir: Path):
307
307
 
308
308
  info_line("")
309
309
  info_line("Which AI tool are you configuring?")
310
- tool_options = ["VS Code (Copilot)", "Cursor", "Claude Code"]
311
- if shutil.which("cortex"):
312
- tool_options.append("Cortex Code")
310
+ tool_options = ["VS Code (Copilot)", "Cursor", "Claude Code", "Cortex Code"]
313
311
  tool_options.append("Skip")
314
312
  tool_choice = select_from_list(tool_options, "tool", add_back=False)
315
313
 
316
314
  if tool_choice == "Cortex Code":
317
315
  info_line("")
318
316
  info_line("Adding MCP servers to Cortex Code...")
319
- try:
320
- subprocess.run(["cortex", "mcp", "add", "datasecops-framework", "--",
321
- "datasecops-mcp", "--connection-name", connection_name,
322
- "--app-database", app_database],
323
- capture_output=True, text=True)
324
- success_line("Added datasecops-framework MCP server")
325
- except FileNotFoundError:
326
- warning_line(
327
- "cortex not found — run manually: cortex mcp add datasecops-framework "
328
- f"-- datasecops-mcp --connection-name {connection_name} --app-database {app_database}"
329
- )
317
+ mcp_path = project_dir / ".snowflake" / "cortex" / "mcp.json"
318
+
319
+ servers = {"datasecops-framework": {
320
+ "type": "stdio",
321
+ "command": "datasecops-mcp",
322
+ "args": ["--connection-name", connection_name, "--app-database", app_database],
323
+ }}
330
324
 
331
325
  # Add AI Agent MCP server if enabled
332
326
  if ai_agent_mcp_url:
333
- try:
334
- result = subprocess.run(["cortex", "mcp", "add", "datasecops-docs", ai_agent_mcp_url, "--transport", "http"],
335
- capture_output=True, text=True)
336
- if result.returncode == 0:
337
- success_line("Added datasecops-docs MCP server (AI Agent documentation search)")
338
- else:
339
- warning_line(f"Failed to add AI Agent MCP server: {result.stderr.strip() or 'unknown error'}")
340
- except Exception as agent_err:
341
- warning_line(f"Could not add AI Agent MCP server: {agent_err}")
327
+ snowflake_pat = get_input_string(
328
+ "Enter your Snowflake PAT for datasecops-docs MCP server (or press Enter to skip): ",
329
+ allow_empty=True,
330
+ )
331
+ server_entry = {
332
+ "type": "http",
333
+ "url": ai_agent_mcp_url,
334
+ }
335
+ if snowflake_pat:
336
+ server_entry["headers"] = {"Authorization": f"Bearer {snowflake_pat}"}
337
+ servers["datasecops-docs"] = server_entry
342
338
 
343
339
  if platform_choice == "GitHub":
344
340
  github_token = get_input_string("Enter your GitHub PAT (or press Enter to skip): ", allow_empty=True)
345
341
  if github_token:
346
- try:
347
- subprocess.run(["cortex", "mcp", "add", "github",
348
- "-e", f"GITHUB_PERSONAL_ACCESS_TOKEN={github_token}",
349
- "--", "npx", "-y", "@modelcontextprotocol/server-github"],
350
- capture_output=True, text=True)
351
- success_line("Added GitHub MCP server")
352
- except FileNotFoundError:
353
- warning_line("cortex not found — configure GitHub MCP server manually")
342
+ servers["github"] = {
343
+ "type": "stdio",
344
+ "command": "npx",
345
+ "args": ["-y", "@modelcontextprotocol/server-github"],
346
+ "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": github_token},
347
+ }
354
348
  elif platform_choice == "Azure DevOps":
355
349
  azure_org = get_input_string("Enter your Azure DevOps org URL (e.g. https://dev.azure.com/myorg): ")
356
350
  azure_pat = get_input_string("Enter your Azure DevOps PAT (or press Enter to skip): ", allow_empty=True)
357
351
  if azure_pat:
358
- try:
359
- subprocess.run(["cortex", "mcp", "add", "azure-devops",
360
- "-e", f"AZURE_DEVOPS_ORG_URL={azure_org}",
361
- "-e", "AZURE_DEVOPS_AUTH_METHOD=pat",
362
- "-e", f"AZURE_DEVOPS_PAT={azure_pat}",
363
- "--", "npx", "-y", "@tiberriver256/mcp-server-azure-devops"],
364
- capture_output=True, text=True)
365
- success_line("Added Azure DevOps MCP server")
366
- except FileNotFoundError:
367
- warning_line("cortex not found — configure Azure DevOps MCP server manually")
368
-
369
- success_line("Cortex Code MCP servers configured")
352
+ servers["azure-devops"] = {
353
+ "type": "stdio",
354
+ "command": "npx",
355
+ "args": ["-y", "@tiberriver256/mcp-server-azure-devops"],
356
+ "env": {
357
+ "AZURE_DEVOPS_ORG_URL": azure_org,
358
+ "AZURE_DEVOPS_AUTH_METHOD": "pat",
359
+ "AZURE_DEVOPS_PAT": azure_pat,
360
+ },
361
+ }
362
+
363
+ from datasecops_cli.utilities.file_utils import ensure_dir, write_file
364
+ ensure_dir(mcp_path.parent)
365
+ write_file(mcp_path, json.dumps({"mcpServers": servers}, indent=2))
366
+ success_line(f"MCP config written to {mcp_path}")
370
367
 
371
368
  elif tool_choice in ("VS Code (Copilot)", "Cursor", "Claude Code"):
372
369
  dir_map = {
@@ -150,9 +150,7 @@ class ConfigurationMenu:
150
150
  # Select target tool
151
151
  info_line("")
152
152
  info_line("Which AI tool are you configuring?")
153
- tool_options = ["VS Code (Copilot)", "Cursor", "Claude Code"]
154
- if shutil.which("cortex"):
155
- tool_options.append("Cortex Code")
153
+ tool_options = ["VS Code (Copilot)", "Cursor", "Claude Code", "Cortex Code"]
156
154
  tool_choice = select_from_list(tool_options, "tool", add_back=False)
157
155
 
158
156
  if tool_choice == "Cortex Code":
@@ -163,60 +161,16 @@ class ConfigurationMenu:
163
161
  complete_action()
164
162
 
165
163
  def _install_mcp_cortex(self, servers: list[str], platform: str) -> None:
166
- """Install MCP servers via cortex mcp add."""
167
- info_line("")
168
- for server in servers:
169
- if server == "datasecops-framework":
170
- conn = self.datasecops_config.connection_name
171
- app_db = self.datasecops_config.app_database
172
- try:
173
- subprocess.run(["cortex", "mcp", "add", "datasecops-framework", "--",
174
- "datasecops-mcp", "--connection-name", conn,
175
- "--app-database", app_db],
176
- capture_output=True, text=True)
177
- success_line("Added datasecops-framework MCP server")
178
- except FileNotFoundError:
179
- warning_line(
180
- "cortex not found — run manually: cortex mcp add datasecops-framework "
181
- f"-- datasecops-mcp --connection-name {conn} --app-database {app_db}"
182
- )
183
-
184
- elif server == "GitHub":
185
- token = get_input_string("Enter your GitHub PAT (or press Enter to skip): ", allow_empty=True)
186
- if token:
187
- try:
188
- subprocess.run(["cortex", "mcp", "add", "github",
189
- "-e", f"GITHUB_PERSONAL_ACCESS_TOKEN={token}",
190
- "--", "npx", "-y", "@modelcontextprotocol/server-github"],
191
- capture_output=True, text=True)
192
- success_line("Added GitHub MCP server")
193
- except FileNotFoundError:
194
- warning_line("cortex not found — configure GitHub MCP server manually")
195
-
196
- elif server == "Azure DevOps":
197
- org = get_input_string("Enter your Azure DevOps org URL (e.g. https://dev.azure.com/myorg): ")
198
- pat = get_input_string("Enter your Azure DevOps PAT (or press Enter to skip): ", allow_empty=True)
199
- if pat:
200
- try:
201
- subprocess.run(["cortex", "mcp", "add", "azure-devops",
202
- "-e", f"AZURE_DEVOPS_ORG_URL={org}",
203
- "-e", "AZURE_DEVOPS_AUTH_METHOD=pat",
204
- "-e", f"AZURE_DEVOPS_PAT={pat}",
205
- "--", "npx", "-y", "@tiberriver256/mcp-server-azure-devops"],
206
- capture_output=True, text=True)
207
- success_line("Added Azure DevOps MCP server")
208
- except FileNotFoundError:
209
- warning_line("cortex not found — configure Azure DevOps MCP server manually")
210
-
211
- elif server == "dbt":
212
- try:
213
- subprocess.run(["cortex", "mcp", "add", "dbt", "--", "uvx", "dbt-mcp"],
214
- capture_output=True, text=True)
215
- success_line("Added dbt MCP server")
216
- except FileNotFoundError:
217
- warning_line("cortex not found — run manually: cortex mcp add dbt -- uvx dbt-mcp")
218
-
219
- success_line("Cortex Code MCP servers configured")
164
+ """Write MCP server config to .snowflake/cortex/mcp.json for Cortex Code."""
165
+ from datasecops_cli.utilities.mcp import write_cortex_mcp_config
166
+
167
+ mcp_path = self.project_dir / ".snowflake" / "cortex" / "mcp.json"
168
+ write_cortex_mcp_config(
169
+ mcp_path=mcp_path,
170
+ servers=servers,
171
+ connection_name=self.datasecops_config.connection_name,
172
+ app_database=self.datasecops_config.app_database,
173
+ )
220
174
 
221
175
  def _install_mcp_json(self, servers: list[str], platform: str, tool: str) -> None:
222
176
  """Write MCP server config as mcp.json for VS Code, Cursor, or Claude Code."""
@@ -164,9 +164,30 @@ class DevelopmentMenu:
164
164
  info_line("No .sqlfluff config found — downloading from framework...")
165
165
  if not self.downloads.download_sqlfluff_config(profiles_dir=self.profiles_dir, dbt_project_dir=self.linting.project_dir):
166
166
  return False
167
- # Fetch pinned versions from the native app and ensure they're installed
168
- required = self.downloads.get_sqlfluff_requirements() if self.downloads else None
169
- return self.linting.ensure_templater_installed(required_packages=required or None)
167
+ # Fetch pinned versions from the native app (sqlfluff + dbt) and ensure they're installed
168
+ required = []
169
+ if self.downloads:
170
+ sqlfluff_pkgs = self.downloads.get_sqlfluff_requirements() or []
171
+ dbt_pkgs = self.downloads.get_dbt_requirements() or []
172
+ required = sqlfluff_pkgs + dbt_pkgs
173
+ if not self.linting.ensure_templater_installed(required_packages=required or None):
174
+ return False
175
+ # Ensure dbt project packages are installed (required by sqlfluff dbt templater)
176
+ self._ensure_dbt_packages()
177
+ return True
178
+
179
+ def _ensure_dbt_packages(self) -> None:
180
+ """Ensure dbt packages are installed for the sqlfluff dbt templater."""
181
+ packages_dir = self.linting.project_dir / "dbt_packages"
182
+ if packages_dir.exists() and any(packages_dir.iterdir()):
183
+ return
184
+
185
+ packages_yml = self.linting.project_dir / "packages.yml"
186
+ if not packages_yml.exists():
187
+ return
188
+
189
+ info_line("Installing dbt packages (required for linting)...")
190
+ self.dbt.deps()
170
191
 
171
192
  def _lint_menu(self) -> None:
172
193
  clear()
@@ -134,9 +134,7 @@ class DownloadsMenu:
134
134
  # Select target tool
135
135
  info_line("")
136
136
  info_line("Which AI tool are you configuring?")
137
- tool_options = ["VS Code (Copilot)", "Cursor", "Claude Code"]
138
- if shutil.which("cortex"):
139
- tool_options.append("Cortex Code")
137
+ tool_options = ["VS Code (Copilot)", "Cursor", "Claude Code", "Cortex Code"]
140
138
  tool_choice = select_from_list(tool_options, "tool", add_back=False)
141
139
 
142
140
  if tool_choice == "Cortex Code":
@@ -147,60 +145,16 @@ class DownloadsMenu:
147
145
  complete_action()
148
146
 
149
147
  def _install_mcp_cortex(self, servers: list[str], platform: str) -> None:
150
- """Install MCP servers via cortex mcp add."""
151
- info_line("")
152
- for server in servers:
153
- if server == "datasecops-framework":
154
- conn = self.datasecops_config.connection_name
155
- app_db = self.datasecops_config.app_database
156
- try:
157
- subprocess.run(["cortex", "mcp", "add", "datasecops-framework", "--",
158
- "datasecops-mcp", "--connection-name", conn,
159
- "--app-database", app_db],
160
- capture_output=True, text=True)
161
- success_line("Added datasecops-framework MCP server")
162
- except FileNotFoundError:
163
- warning_line(
164
- "cortex not found — run manually: cortex mcp add datasecops-framework "
165
- f"-- datasecops-mcp --connection-name {conn} --app-database {app_db}"
166
- )
167
-
168
- elif server == "GitHub":
169
- token = get_input_string("Enter your GitHub PAT (or press Enter to skip): ", allow_empty=True)
170
- if token:
171
- try:
172
- subprocess.run(["cortex", "mcp", "add", "github",
173
- "-e", f"GITHUB_PERSONAL_ACCESS_TOKEN={token}",
174
- "--", "npx", "-y", "@modelcontextprotocol/server-github"],
175
- capture_output=True, text=True)
176
- success_line("Added GitHub MCP server")
177
- except FileNotFoundError:
178
- warning_line("cortex not found — configure GitHub MCP server manually")
179
-
180
- elif server == "Azure DevOps":
181
- org = get_input_string("Enter your Azure DevOps org URL (e.g. https://dev.azure.com/myorg): ")
182
- pat = get_input_string("Enter your Azure DevOps PAT (or press Enter to skip): ", allow_empty=True)
183
- if pat:
184
- try:
185
- subprocess.run(["cortex", "mcp", "add", "azure-devops",
186
- "-e", f"AZURE_DEVOPS_ORG_URL={org}",
187
- "-e", "AZURE_DEVOPS_AUTH_METHOD=pat",
188
- "-e", f"AZURE_DEVOPS_PAT={pat}",
189
- "--", "npx", "-y", "@tiberriver256/mcp-server-azure-devops"],
190
- capture_output=True, text=True)
191
- success_line("Added Azure DevOps MCP server")
192
- except FileNotFoundError:
193
- warning_line("cortex not found — configure Azure DevOps MCP server manually")
194
-
195
- elif server == "dbt":
196
- try:
197
- subprocess.run(["cortex", "mcp", "add", "dbt", "--", "uvx", "dbt-mcp"],
198
- capture_output=True, text=True)
199
- success_line("Added dbt MCP server")
200
- except FileNotFoundError:
201
- warning_line("cortex not found — run manually: cortex mcp add dbt -- uvx dbt-mcp")
202
-
203
- success_line("Cortex Code MCP servers configured")
148
+ """Write MCP server config to .snowflake/cortex/mcp.json for Cortex Code."""
149
+ from datasecops_cli.utilities.mcp import write_cortex_mcp_config
150
+
151
+ mcp_path = Path.cwd() / ".snowflake" / "cortex" / "mcp.json"
152
+ write_cortex_mcp_config(
153
+ mcp_path=mcp_path,
154
+ servers=servers,
155
+ connection_name=self.datasecops_config.connection_name,
156
+ app_database=self.datasecops_config.app_database,
157
+ )
204
158
 
205
159
  def _install_mcp_json(self, servers: list[str], platform: str, tool: str) -> None:
206
160
  """Write MCP server config as mcp.json for VS Code, Cursor, or Claude Code."""
@@ -82,7 +82,7 @@ class LintingService:
82
82
  return self.install_requirements(to_install)
83
83
 
84
84
  # No pinned versions — just ensure sqlfluff packages are present
85
- missing = [pkg for pkg in ["sqlfluff", "sqlfluff-templater-dbt"] if not installed.get(pkg)]
85
+ missing = [pkg for pkg in ["sqlfluff", "sqlfluff-templater-dbt", "dbt-snowflake"] if not installed.get(pkg)]
86
86
  if not missing:
87
87
  return True
88
88
  warning_line(f"Missing packages: {', '.join(missing)}")
@@ -99,6 +99,9 @@ class LintingService:
99
99
  config_path = self.project_dir / ".sqlfluff"
100
100
  if config_path.exists():
101
101
  cmd += ["--config", str(config_path)]
102
+
103
+ # Ensure .sqlfluffignore is created with defaults if it doesn't exist
104
+ self._ensure_sqlfluffignore()
102
105
 
103
106
  info_line(f"Running: sqlfluff {action}")
104
107
  result = subprocess.run(cmd, capture_output=False, cwd=str(self.project_dir))
@@ -109,6 +112,31 @@ class LintingService:
109
112
  else:
110
113
  error_line(f"SQLFluff {action} failed with exit code {result.returncode}")
111
114
  return result
115
+
116
+ def _ensure_sqlfluffignore(self) -> None:
117
+ """Create a default .sqlfluffignore if one doesn't exist."""
118
+ ignore_path = self.project_dir / ".sqlfluffignore"
119
+ if ignore_path.exists():
120
+ return
121
+ default_content = "\n".join([
122
+ "# Directories to exclude from linting",
123
+ "target/",
124
+ "dbt_packages/",
125
+ "dbt_modules/",
126
+ "logs/",
127
+ "macros/",
128
+ "",
129
+ "# Ignore cortex agents and semantic views - spec is part of the body",
130
+ "models/cortex/agents",
131
+ "models/cortex/semantic_views",
132
+ "",
133
+ "# Ignore config-only object directories",
134
+ "objects/stages/config/",
135
+ "objects/file_formats/config/",
136
+ "",
137
+ ])
138
+ ignore_path.write_text(default_content, encoding="utf-8")
139
+ info_line(f"Created default .sqlfluffignore at {ignore_path}")
112
140
 
113
141
  def lint_modified(self, fix: bool = False, changed_files: list[str] = None) -> None:
114
142
  if not changed_files:
@@ -0,0 +1,74 @@
1
+ """MCP server configuration utilities."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from datasecops_cli.utilities.display import get_input_string, success_line
7
+ from datasecops_cli.utilities.file_utils import ensure_dir, write_file
8
+
9
+
10
+ def build_cortex_mcp_servers(
11
+ servers: list[str],
12
+ connection_name: str,
13
+ app_database: str,
14
+ ) -> dict:
15
+ """Build an MCP server config dict for selected servers, prompting for credentials.
16
+
17
+ Returns a dict suitable for {"mcpServers": ...} JSON output.
18
+ """
19
+ server_config = {}
20
+
21
+ for server in servers:
22
+ if server == "datasecops-framework":
23
+ server_config["datasecops-framework"] = {
24
+ "type": "stdio",
25
+ "command": "datasecops-mcp",
26
+ "args": ["--connection-name", connection_name, "--app-database", app_database],
27
+ }
28
+
29
+ elif server == "GitHub":
30
+ token = get_input_string("Enter your GitHub PAT (or press Enter to skip): ", allow_empty=True)
31
+ if token:
32
+ server_config["github"] = {
33
+ "type": "stdio",
34
+ "command": "npx",
35
+ "args": ["-y", "@modelcontextprotocol/server-github"],
36
+ "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": token},
37
+ }
38
+
39
+ elif server == "Azure DevOps":
40
+ org = get_input_string("Enter your Azure DevOps org URL (e.g. https://dev.azure.com/myorg): ")
41
+ pat = get_input_string("Enter your Azure DevOps PAT (or press Enter to skip): ", allow_empty=True)
42
+ if pat:
43
+ server_config["azure-devops"] = {
44
+ "type": "stdio",
45
+ "command": "npx",
46
+ "args": ["-y", "@tiberriver256/mcp-server-azure-devops"],
47
+ "env": {
48
+ "AZURE_DEVOPS_ORG_URL": org,
49
+ "AZURE_DEVOPS_AUTH_METHOD": "pat",
50
+ "AZURE_DEVOPS_PAT": pat,
51
+ },
52
+ }
53
+
54
+ elif server == "dbt":
55
+ server_config["dbt"] = {
56
+ "type": "stdio",
57
+ "command": "uvx",
58
+ "args": ["dbt-mcp"],
59
+ }
60
+
61
+ return server_config
62
+
63
+
64
+ def write_cortex_mcp_config(
65
+ mcp_path: Path,
66
+ servers: list[str],
67
+ connection_name: str,
68
+ app_database: str,
69
+ ) -> None:
70
+ """Build and write .snowflake/cortex/mcp.json for Cortex Code."""
71
+ server_config = build_cortex_mcp_servers(servers, connection_name, app_database)
72
+ ensure_dir(mcp_path.parent)
73
+ write_file(mcp_path, json.dumps({"mcpServers": server_config}, indent=2))
74
+ success_line(f"MCP config written to {mcp_path}")
@@ -79,6 +79,8 @@ class MCPSnowflakeService:
79
79
  "get_source_definitions",
80
80
  "get_access_groups",
81
81
  "get_support_contacts",
82
+ "get_sqlfluff_config_file",
83
+ "get_documentation",
82
84
  })
83
85
 
84
86
  def _call_procedure(self, proc_name: str, *args) -> Any:
@@ -118,6 +120,12 @@ class MCPSnowflakeService:
118
120
  def get_support_contacts(self) -> list:
119
121
  return self._call_procedure("get_support_contacts") or []
120
122
 
123
+ def get_sqlfluff_config_file(self) -> str:
124
+ return self._call_procedure("get_sqlfluff_config_file") or ""
125
+
126
+ def get_documentation(self) -> list:
127
+ return self._call_procedure("get_documentation") or []
128
+
121
129
  def close(self):
122
130
  if self._conn:
123
131
  self._conn.close()
@@ -8,7 +8,7 @@ dbt packages, project profiles, and deployment settings in real-time.
8
8
  import json
9
9
  import logging
10
10
 
11
- from mcp.server.fastmcp import FastMCP
11
+ from mcp.server.fastmcp import Context, FastMCP
12
12
 
13
13
  from datasecops_mcp.connection import get_snowflake_service
14
14
  from datasecops_cli.models.project_config import DEFAULT_BRANCH_FORMAT
@@ -311,6 +311,36 @@ def get_support_contacts() -> str:
311
311
  return json.dumps(raw, indent=2)
312
312
 
313
313
 
314
+ @mcp.tool()
315
+ def get_sqlfluff_config() -> str:
316
+ """Get the rendered SQLFluff configuration file (.sqlfluff INI format).
317
+
318
+ Returns the fully rendered .sqlfluff config file content as an INI string.
319
+ This is the actual config that should be written to the project's .sqlfluff file.
320
+ Use this when you need the exact linting configuration for the project.
321
+ """
322
+ sf = get_snowflake_service()
323
+ raw = sf.get_sqlfluff_config_file()
324
+ if not raw:
325
+ return "No rendered SQLFluff config found in native app"
326
+ return raw
327
+
328
+
329
+ @mcp.tool()
330
+ def get_documentation() -> str:
331
+ """Get all documentation topics from the framework.
332
+
333
+ Returns documentation topics with their content, covering areas like
334
+ model development, object development, data governance, and deployment.
335
+ Use this to understand framework conventions and best practices.
336
+ """
337
+ sf = get_snowflake_service()
338
+ raw = sf.get_documentation()
339
+ if not raw:
340
+ return "No documentation found in native app"
341
+ return json.dumps(raw, indent=2)
342
+
343
+
314
344
  # --- Git / Source Control Tools ---
315
345
 
316
346
 
@@ -399,7 +429,7 @@ def get_deployment_workflow() -> str:
399
429
 
400
430
 
401
431
  @mcp.tool()
402
- def lint_sql(file_path: str) -> str:
432
+ async def lint_sql(file_path: str, ctx: Context) -> str:
403
433
  """Lint a SQL file using SQLFluff with the project's configured rules.
404
434
 
405
435
  Args:
@@ -408,10 +438,15 @@ def lint_sql(file_path: str) -> str:
408
438
  Returns the linting results including rule violations, line numbers,
409
439
  and descriptions. Uses the project's .sqlfluff config from the framework.
410
440
  """
441
+ import asyncio
411
442
  import subprocess
412
443
  from pathlib import Path
413
444
 
445
+ await ctx.report_progress(0, 3)
446
+ await ctx.info(f"Finding dbt project for: {file_path}")
414
447
  project_dir = _find_project_dir()
448
+ _ensure_dbt_packages(project_dir)
449
+
415
450
  target = Path(file_path)
416
451
  if not target.is_absolute():
417
452
  target = project_dir / target
@@ -419,12 +454,20 @@ def lint_sql(file_path: str) -> str:
419
454
  if not target.exists():
420
455
  return json.dumps({"error": f"File not found: {target}"})
421
456
 
457
+ await ctx.report_progress(1, 3)
458
+ await ctx.info(f"Running SQLFluff lint on {target.name}...")
459
+
422
460
  cmd = ["sqlfluff", "lint", str(target), "--format", "json"]
423
461
  config_path = project_dir / ".sqlfluff"
424
462
  if config_path.exists():
425
463
  cmd += ["--config", str(config_path)]
426
464
 
427
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(project_dir))
465
+ result = await asyncio.to_thread(
466
+ subprocess.run, cmd, capture_output=True, text=True, cwd=str(project_dir)
467
+ )
468
+
469
+ await ctx.report_progress(2, 3)
470
+ await ctx.info("Processing lint results...")
428
471
 
429
472
  # SQLFluff returns exit code 1 when violations are found (not an error)
430
473
  if result.returncode > 1:
@@ -433,14 +476,17 @@ def lint_sql(file_path: str) -> str:
433
476
  if result.stdout.strip():
434
477
  try:
435
478
  violations = json.loads(result.stdout)
479
+ await ctx.report_progress(3, 3)
436
480
  return json.dumps(violations, indent=2)
437
481
  except json.JSONDecodeError:
482
+ await ctx.report_progress(3, 3)
438
483
  return result.stdout
484
+ await ctx.report_progress(3, 3)
439
485
  return json.dumps({"status": "clean", "message": "No linting issues found"})
440
486
 
441
487
 
442
488
  @mcp.tool()
443
- def fix_sql(file_path: str) -> str:
489
+ async def fix_sql(file_path: str, ctx: Context) -> str:
444
490
  """Auto-fix linting issues in a SQL file using SQLFluff.
445
491
 
446
492
  Args:
@@ -449,10 +495,15 @@ def fix_sql(file_path: str) -> str:
449
495
  Applies automatic fixes using the project's .sqlfluff config.
450
496
  Returns the fix results and what was changed.
451
497
  """
498
+ import asyncio
452
499
  import subprocess
453
500
  from pathlib import Path
454
501
 
502
+ await ctx.report_progress(0, 3)
503
+ await ctx.info(f"Finding dbt project for: {file_path}")
455
504
  project_dir = _find_project_dir()
505
+ _ensure_dbt_packages(project_dir)
506
+
456
507
  target = Path(file_path)
457
508
  if not target.is_absolute():
458
509
  target = project_dir / target
@@ -460,12 +511,20 @@ def fix_sql(file_path: str) -> str:
460
511
  if not target.exists():
461
512
  return json.dumps({"error": f"File not found: {target}"})
462
513
 
514
+ await ctx.report_progress(1, 3)
515
+ await ctx.info(f"Running SQLFluff fix on {target.name}...")
516
+
463
517
  cmd = ["sqlfluff", "fix", str(target), "--force", "--format", "json"]
464
518
  config_path = project_dir / ".sqlfluff"
465
519
  if config_path.exists():
466
520
  cmd += ["--config", str(config_path)]
467
521
 
468
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(project_dir))
522
+ result = await asyncio.to_thread(
523
+ subprocess.run, cmd, capture_output=True, text=True, cwd=str(project_dir)
524
+ )
525
+
526
+ await ctx.report_progress(2, 3)
527
+ await ctx.info("Processing fix results...")
469
528
 
470
529
  if result.returncode > 1:
471
530
  return json.dumps({"error": f"SQLFluff fix failed: {result.stderr.strip()}"})
@@ -473,14 +532,17 @@ def fix_sql(file_path: str) -> str:
473
532
  if result.stdout.strip():
474
533
  try:
475
534
  fix_results = json.loads(result.stdout)
535
+ await ctx.report_progress(3, 3)
476
536
  return json.dumps(fix_results, indent=2)
477
537
  except json.JSONDecodeError:
538
+ await ctx.report_progress(3, 3)
478
539
  return result.stdout
540
+ await ctx.report_progress(3, 3)
479
541
  return json.dumps({"status": "fixed", "message": f"Fixes applied to {target.name}"})
480
542
 
481
543
 
482
544
  @mcp.tool()
483
- def lint_project(fix: bool = False) -> str:
545
+ async def lint_project(fix: bool, ctx: Context) -> str:
484
546
  """Lint all SQL models in the dbt project.
485
547
 
486
548
  Args:
@@ -489,15 +551,23 @@ def lint_project(fix: bool = False) -> str:
489
551
  Lints the models/ directory using the project's .sqlfluff config.
490
552
  Returns a summary of violations or fixes applied.
491
553
  """
554
+ import asyncio
492
555
  import subprocess
493
556
  from pathlib import Path
494
557
 
558
+ action = "fix" if fix else "lint"
559
+
560
+ await ctx.report_progress(0, 3)
561
+ await ctx.info("Finding dbt project...")
495
562
  project_dir = _find_project_dir()
563
+ _ensure_dbt_packages(project_dir)
496
564
  models_dir = project_dir / "models"
497
565
  if not models_dir.exists():
498
566
  return json.dumps({"error": f"No models directory found at {models_dir}"})
499
567
 
500
- action = "fix" if fix else "lint"
568
+ await ctx.report_progress(1, 3)
569
+ await ctx.info(f"Running SQLFluff {action} on models/...")
570
+
501
571
  cmd = ["sqlfluff", action, str(models_dir), "--format", "json"]
502
572
  if fix:
503
573
  cmd.append("--force")
@@ -506,7 +576,12 @@ def lint_project(fix: bool = False) -> str:
506
576
  if config_path.exists():
507
577
  cmd += ["--config", str(config_path)]
508
578
 
509
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(project_dir))
579
+ result = await asyncio.to_thread(
580
+ subprocess.run, cmd, capture_output=True, text=True, cwd=str(project_dir)
581
+ )
582
+
583
+ await ctx.report_progress(2, 3)
584
+ await ctx.info("Processing results...")
510
585
 
511
586
  if result.returncode > 1:
512
587
  return json.dumps({"error": f"SQLFluff {action} failed: {result.stderr.strip()}"})
@@ -527,10 +602,14 @@ def lint_project(fix: bool = False) -> str:
527
602
  "truncated": True,
528
603
  "message": f"Showing first 5 of {len(output)} files",
529
604
  }
605
+ await ctx.report_progress(3, 3)
530
606
  return json.dumps(summary, indent=2)
607
+ await ctx.report_progress(3, 3)
531
608
  return json.dumps(output, indent=2)
532
609
  except json.JSONDecodeError:
610
+ await ctx.report_progress(3, 3)
533
611
  return result.stdout
612
+ await ctx.report_progress(3, 3)
534
613
  return json.dumps({"status": "clean", "action": action, "message": "No issues found"})
535
614
 
536
615
 
@@ -546,6 +625,48 @@ def _find_project_dir() -> "Path":
546
625
  return cwd
547
626
 
548
627
 
628
+ def _ensure_dbt_packages(project_dir: "Path") -> None:
629
+ """Ensure dbt-snowflake and dbt packages are available for sqlfluff dbt templater.
630
+
631
+ Skips expensive dbt deps if packages are already present, but always
632
+ ensures the dbt-snowflake Python package is importable.
633
+ """
634
+ import subprocess
635
+ from pathlib import Path
636
+
637
+ # Always ensure dbt-snowflake Python package is available (required by sqlfluff-templater-dbt)
638
+ try:
639
+ import dbt.adapters.snowflake # noqa: F401
640
+ except ImportError:
641
+ subprocess.run(
642
+ ["uv", "pip", "install", "dbt-snowflake"],
643
+ capture_output=True, text=True,
644
+ )
645
+
646
+ # Ensure dbt project packages are installed (refs to macros in packages)
647
+ packages_dir = project_dir / "dbt_packages"
648
+ if packages_dir.exists() and any(packages_dir.iterdir()):
649
+ return # Already installed
650
+
651
+ # Also check legacy dbt_modules directory
652
+ modules_dir = project_dir / "dbt_modules"
653
+ if modules_dir.exists() and any(modules_dir.iterdir()):
654
+ return
655
+
656
+ # Check if packages.yml exists before running deps
657
+ packages_yml = project_dir / "packages.yml"
658
+ if not packages_yml.exists():
659
+ return # No packages to install
660
+
661
+ # Packages are missing — run dbt deps to install them
662
+ import shutil
663
+
664
+ for cmd in [["dbtf", "deps"], ["dbt", "deps"]]:
665
+ if shutil.which(cmd[0]):
666
+ subprocess.run(cmd, capture_output=True, text=True, cwd=str(project_dir))
667
+ return
668
+
669
+
549
670
  def main():
550
671
  """Run the MCP server via stdio transport."""
551
672
  import argparse
@@ -1 +0,0 @@
1
- __version__ = "0.5.1"
File without changes
File without changes
File without changes