detectkit 0.61.0__tar.gz → 0.62.0__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 (169) hide show
  1. {detectkit-0.61.0/detectkit.egg-info → detectkit-0.62.0}/PKG-INFO +18 -1
  2. {detectkit-0.61.0 → detectkit-0.62.0}/README.md +14 -0
  3. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/__init__.py +1 -1
  4. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/cli.md +35 -0
  5. detectkit-0.62.0/detectkit/cli/commands/mcp.py +31 -0
  6. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/main.py +56 -0
  7. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/config/profile.py +22 -3
  8. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/_sql_manager.py +9 -2
  9. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/clickhouse_manager.py +11 -5
  10. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/duckdb_manager.py +25 -1
  11. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/mysql_manager.py +5 -0
  12. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/postgres_manager.py +5 -0
  13. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/loaders/metric_loader.py +7 -1
  14. detectkit-0.62.0/detectkit/mcp/__init__.py +47 -0
  15. detectkit-0.62.0/detectkit/mcp/context.py +329 -0
  16. detectkit-0.62.0/detectkit/mcp/errors.py +27 -0
  17. detectkit-0.62.0/detectkit/mcp/serialize.py +90 -0
  18. detectkit-0.62.0/detectkit/mcp/server.py +263 -0
  19. detectkit-0.62.0/detectkit/mcp/tools.py +502 -0
  20. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/utils/datetime_utils.py +11 -2
  21. {detectkit-0.61.0 → detectkit-0.62.0/detectkit.egg-info}/PKG-INFO +18 -1
  22. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit.egg-info/SOURCES.txt +7 -0
  23. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit.egg-info/requires.txt +4 -0
  24. {detectkit-0.61.0 → detectkit-0.62.0}/pyproject.toml +7 -0
  25. {detectkit-0.61.0 → detectkit-0.62.0}/LICENSE +0 -0
  26. {detectkit-0.61.0 → detectkit-0.62.0}/MANIFEST.in +0 -0
  27. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/__init__.py +0 -0
  28. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/__init__.py +0 -0
  29. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/base.py +0 -0
  30. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/branding.py +0 -0
  31. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/discord.py +0 -0
  32. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/email.py +0 -0
  33. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/factory.py +0 -0
  34. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/googlechat.py +0 -0
  35. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/mattermost.py +0 -0
  36. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/ntfy.py +0 -0
  37. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/slack.py +0 -0
  38. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/teams.py +0 -0
  39. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/telegram.py +0 -0
  40. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/channels/webhook.py +0 -0
  41. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  42. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  43. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  44. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  45. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  46. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  47. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  48. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  49. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  50. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/__init__.py +0 -0
  51. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/_base.py +0 -0
  52. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/_types.py +0 -0
  53. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/autotuner.py +0 -0
  54. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/axis_spec.py +0 -0
  55. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/config_emitter.py +0 -0
  56. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/crossval.py +0 -0
  57. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/detector_select.py +0 -0
  58. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/distribution.py +0 -0
  59. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/grid_search.py +0 -0
  60. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/labels.py +0 -0
  61. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/result.py +0 -0
  62. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/runner.py +0 -0
  63. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/scoring.py +0 -0
  64. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/seasonality_search.py +0 -0
  65. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/settings.py +0 -0
  66. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/autotune/window_select.py +0 -0
  67. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/__init__.py +0 -0
  68. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/_output.py +0 -0
  69. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  70. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  71. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  72. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  73. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  74. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  75. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  76. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  77. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  78. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  79. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  80. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  81. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/__init__.py +0 -0
  82. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/autotune.py +0 -0
  83. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/clean.py +0 -0
  84. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/init.py +0 -0
  85. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/init_claude.py +0 -0
  86. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/osi.py +0 -0
  87. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/run.py +0 -0
  88. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/test_alert.py +0 -0
  89. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/tune.py +0 -0
  90. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/ui.py +0 -0
  91. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/cli/commands/unlock.py +0 -0
  92. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/config/__init__.py +0 -0
  93. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/config/metric_config.py +0 -0
  94. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/config/metric_io.py +0 -0
  95. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/config/project_config.py +0 -0
  96. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/config/validator.py +0 -0
  97. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/core/__init__.py +0 -0
  98. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/core/interval.py +0 -0
  99. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/core/models.py +0 -0
  100. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/__init__.py +0 -0
  101. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/__init__.py +0 -0
  102. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  103. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  104. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_base.py +0 -0
  105. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  106. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_detections.py +0 -0
  107. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  108. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  109. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_schema.py +0 -0
  110. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  111. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/internal_tables/manager.py +0 -0
  112. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/manager.py +0 -0
  113. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/database/tables.py +0 -0
  114. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/__init__.py +0 -0
  115. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/base.py +0 -0
  116. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/factory.py +0 -0
  117. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/seasonality.py +0 -0
  118. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/statistical/__init__.py +0 -0
  119. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  120. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/statistical/autoreg.py +0 -0
  121. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/statistical/iqr.py +0 -0
  122. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/statistical/mad.py +0 -0
  123. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  124. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/detectors/statistical/zscore.py +0 -0
  125. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/loaders/__init__.py +0 -0
  126. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/loaders/errors.py +0 -0
  127. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/loaders/query_template.py +0 -0
  128. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/__init__.py +0 -0
  129. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/error_dispatch.py +0 -0
  130. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  131. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  132. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  133. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  134. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  135. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  136. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  137. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/reporting/__init__.py +0 -0
  138. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/reporting/assets/report.js +0 -0
  139. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/reporting/builder.py +0 -0
  140. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/reporting/html_report.py +0 -0
  141. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/semantic/__init__.py +0 -0
  142. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/semantic/errors.py +0 -0
  143. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/semantic/exporter.py +0 -0
  144. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/semantic/importer.py +0 -0
  145. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/semantic/osi_model.py +0 -0
  146. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/semantic/query_gen.py +0 -0
  147. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/tuning/__init__.py +0 -0
  148. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/tuning/assets/tune.js +0 -0
  149. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/tuning/config_writer.py +0 -0
  150. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/tuning/html.py +0 -0
  151. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/tuning/payload.py +0 -0
  152. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/tuning/server.py +0 -0
  153. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/ui/__init__.py +0 -0
  154. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/ui/assets/ui.js +0 -0
  155. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/ui/html.py +0 -0
  156. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/ui/jobs.py +0 -0
  157. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/ui/metric_files.py +0 -0
  158. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/ui/overview.py +0 -0
  159. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/ui/server.py +0 -0
  160. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/utils/__init__.py +0 -0
  161. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/utils/env_interpolation.py +0 -0
  162. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/utils/json_utils.py +0 -0
  163. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit/utils/stats.py +0 -0
  164. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit.egg-info/dependency_links.txt +0 -0
  165. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit.egg-info/entry_points.txt +0 -0
  166. {detectkit-0.61.0 → detectkit-0.62.0}/detectkit.egg-info/top_level.txt +0 -0
  167. {detectkit-0.61.0 → detectkit-0.62.0}/requirements.txt +0 -0
  168. {detectkit-0.61.0 → detectkit-0.62.0}/setup.cfg +0 -0
  169. {detectkit-0.61.0 → detectkit-0.62.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.61.0
3
+ Version: 0.62.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -54,6 +54,8 @@ Requires-Dist: prophet>=1.1.0; extra == "advanced-detectors"
54
54
  Requires-Dist: timesfm>=0.1.0; extra == "advanced-detectors"
55
55
  Provides-Extra: osi
56
56
  Requires-Dist: sqlglot>=20.0; extra == "osi"
57
+ Provides-Extra: mcp
58
+ Requires-Dist: mcp<2,>=1.27; extra == "mcp"
57
59
  Provides-Extra: all
58
60
  Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
59
61
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
@@ -62,6 +64,7 @@ Requires-Dist: duckdb>=1.1; extra == "all"
62
64
  Requires-Dist: prophet>=1.1.0; extra == "all"
63
65
  Requires-Dist: timesfm>=0.1.0; extra == "all"
64
66
  Requires-Dist: sqlglot>=20.0; extra == "all"
67
+ Requires-Dist: mcp<2,>=1.27; extra == "all"
65
68
  Provides-Extra: dev
66
69
  Requires-Dist: pytest>=7.0; extra == "dev"
67
70
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -199,6 +202,18 @@ for r in results:
199
202
  print(f"Anomaly at {r.timestamp}: {r.value}")
200
203
  ```
201
204
 
205
+ ## AI & automation
206
+
207
+ - **[MCP server](docs/guides/mcp.md)** (`dtk mcp`) — a read-only [Model Context
208
+ Protocol](https://modelcontextprotocol.io) server so an AI assistant (Claude
209
+ Code, Claude Desktop, any MCP client) can query a project's metric configs,
210
+ loaded data, detector results, replayed alert history and autotune runs
211
+ directly — no write access to the pipeline.
212
+ - **[GitHub Action](docs/guides/github-action.md)** — a composite action that
213
+ runs `dtk run`/`autotune`/`clean` as a CI check or scheduled job and gates
214
+ on detectkit's own exit-code contract, with the `--json` run summary wired
215
+ to step outputs.
216
+
202
217
  ## Documentation
203
218
 
204
219
  - **[dtk.pipelab.dev](https://dtk.pipelab.dev)** — full docs site, guides, reference, and a live in-browser playground
@@ -207,6 +222,8 @@ for r in results:
207
222
  - [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
208
223
  - [Alerting Guide](docs/guides/alerting.md) — channels, mentions, cooldown, recovery
209
224
  - [Reading Alerts](docs/guides/reading-alerts.md) — what a received alert means (for stakeholders)
225
+ - [MCP Guide](docs/guides/mcp.md) — read-only AI-assistant access with `dtk mcp`
226
+ - [GitHub Action Guide](docs/guides/github-action.md) — run the pipeline in CI
210
227
  - [CLI Reference](docs/reference/cli.md) — command-line documentation
211
228
  - [Examples](docs/examples/) — real-world monitoring scenarios
212
229
  - [Changelog](CHANGELOG.md) — version history
@@ -120,6 +120,18 @@ for r in results:
120
120
  print(f"Anomaly at {r.timestamp}: {r.value}")
121
121
  ```
122
122
 
123
+ ## AI & automation
124
+
125
+ - **[MCP server](docs/guides/mcp.md)** (`dtk mcp`) — a read-only [Model Context
126
+ Protocol](https://modelcontextprotocol.io) server so an AI assistant (Claude
127
+ Code, Claude Desktop, any MCP client) can query a project's metric configs,
128
+ loaded data, detector results, replayed alert history and autotune runs
129
+ directly — no write access to the pipeline.
130
+ - **[GitHub Action](docs/guides/github-action.md)** — a composite action that
131
+ runs `dtk run`/`autotune`/`clean` as a CI check or scheduled job and gates
132
+ on detectkit's own exit-code contract, with the `--json` run summary wired
133
+ to step outputs.
134
+
123
135
  ## Documentation
124
136
 
125
137
  - **[dtk.pipelab.dev](https://dtk.pipelab.dev)** — full docs site, guides, reference, and a live in-browser playground
@@ -128,6 +140,8 @@ for r in results:
128
140
  - [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
129
141
  - [Alerting Guide](docs/guides/alerting.md) — channels, mentions, cooldown, recovery
130
142
  - [Reading Alerts](docs/guides/reading-alerts.md) — what a received alert means (for stakeholders)
143
+ - [MCP Guide](docs/guides/mcp.md) — read-only AI-assistant access with `dtk mcp`
144
+ - [GitHub Action Guide](docs/guides/github-action.md) — run the pipeline in CI
131
145
  - [CLI Reference](docs/reference/cli.md) — command-line documentation
132
146
  - [Examples](docs/examples/) — real-world monitoring scenarios
133
147
  - [Changelog](CHANGELOG.md) — version history
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
4
4
  A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
5
5
  """
6
6
 
7
- __version__ = "0.61.0"
7
+ __version__ = "0.62.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -18,6 +18,7 @@ Run all commands from a project directory (the one containing
18
18
  | `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
19
19
  | `dtk osi import <model>` | Scaffold a native metric from an OSI semantic-model metric (see "OSI interop") |
20
20
  | `dtk osi export` | Publish metrics into an OSI fragment (config in `custom_extensions[detectkit]`) |
21
+ | `dtk mcp` | Serve a **read-only** MCP server for an AI assistant (metrics, data, alerts, autotune history — no writes) |
21
22
  | `dtk --version` | Show installed detectkit version |
22
23
 
23
24
  ## Selectors (`--select` / `-s`)
@@ -376,6 +377,40 @@ The `dtk ui` metric Builder's **From OSI** sub-tab does the import
376
377
  interactively (paste a model, pick a metric/target, Compile) through the
377
378
  same code path — see "Managing metrics from the UI" above.
378
379
 
380
+ ## `dtk mcp`
381
+
382
+ A **read-only** [Model Context Protocol](https://modelcontextprotocol.io)
383
+ server, exposing a project to *another* AI assistant (an MCP client — Claude
384
+ Code, Claude Desktop, an IDE extension) rather than being one itself. It is a
385
+ separate, additive command (needs `pip install 'detectkit[mcp]'`) that
386
+ contains **zero write paths**: no config edits, no label writes, no pipeline
387
+ runs, no schema creation. Ten tools: `list_metrics`, `get_metric`,
388
+ `get_metric_status`, `get_project_status`, `query_datapoints`,
389
+ `query_detections`, `replay_alerts` (reconstructs the alert timeline the same
390
+ way `dtk run --report` does — never reads `_dtk_alert_states`, never
391
+ dispatches), `get_autotune_history`, `get_incidents`, `get_server_info`.
392
+ Channel connection details/secrets in `profiles.yml` are never exposed —
393
+ `get_metric` lists alerting channels by name only.
394
+
395
+ ```bash
396
+ dtk mcp --project-dir /abs/path/to/project [--select <sel>] [--profile NAME]
397
+ ```
398
+
399
+ - `--project-dir` — resolved before `$DETECTKIT_PROJECT_DIR` before the
400
+ current directory (an MCP client passes no cwd, so at least one must
401
+ resolve; each mechanism searches upward for `detectkit_project.yml`).
402
+ - `--select` — scopes the whole session: every tool that names a metric
403
+ refuses one outside this set (an access-control boundary, not just a
404
+ default).
405
+
406
+ This is a **different thing** from the generic database MCP CLAUDE.md
407
+ recommends you (the assistant working in this repo) optionally connect to for
408
+ your own read access to the project's DB — `dtk mcp` answers in detectkit's
409
+ own terms (replayed alert decisions, detector identity, labeled incidents),
410
+ not raw SQL, and it's for exposing *this* project to a *separate* assistant
411
+ session/client (e.g. Claude Desktop), not something you need to launch or
412
+ connect to yourself. `dtk mcp --help` has the full flag reference.
413
+
379
414
  ## Common workflows
380
415
 
381
416
  ```bash
@@ -0,0 +1,31 @@
1
+ """Implementation of 'dtk mcp' — the read-only Model Context Protocol server.
2
+
3
+ Isolated from the load/detect/alert pipeline: this module imports only
4
+ ``click`` at load time. Everything else — :mod:`detectkit.mcp.server` and the
5
+ optional ``mcp`` SDK it lazily guards — is imported inside :func:`run_mcp`'s
6
+ body, so `import detectkit.cli.commands.mcp` never pulls in the SDK either.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import click
12
+
13
+
14
+ def run_mcp(*, project_dir: str | None, select: str, profile: str | None) -> None:
15
+ """Resolve the project and serve the read-only MCP server over stdio.
16
+
17
+ Args:
18
+ project_dir: Explicit project directory (searched upward for
19
+ ``detectkit_project.yml``). Falls back to ``$DETECTKIT_PROJECT_DIR``,
20
+ then to searching upward from the current directory.
21
+ select: Selector scoping which metrics the server exposes.
22
+ profile: Profile name to use (defaults to the project's
23
+ ``default_profile``).
24
+ """
25
+ from detectkit.mcp.errors import McpError
26
+ from detectkit.mcp.server import run_server
27
+
28
+ try:
29
+ run_server(project_dir=project_dir, selector=select, profile=profile)
30
+ except McpError as exc:
31
+ raise click.ClickException(str(exc)) from exc
@@ -592,5 +592,61 @@ from detectkit.cli.commands.osi import osi as _osi_group # noqa: E402
592
592
  cli.add_command(_osi_group)
593
593
 
594
594
 
595
+ # MCP (Model Context Protocol) server — a self-contained, READ-ONLY command
596
+ # (`dtk mcp`), unlike `dtk osi` a single command rather than a group. Isolated
597
+ # from the load/detect/alert pipeline: nothing in load/detect/alert imports
598
+ # detectkit.mcp, and this module only imports click at load time — the mcp SDK
599
+ # (the optional `[mcp]` extra) and detectkit.mcp.server are imported lazily,
600
+ # inside the command body.
601
+ @cli.command()
602
+ @click.option(
603
+ "--project-dir",
604
+ default=None,
605
+ help=(
606
+ "Project directory (searched upward for detectkit_project.yml). "
607
+ "Default: $DETECTKIT_PROJECT_DIR, else search upward from the current "
608
+ "directory. An MCP client passes no cwd, so at least one of these must "
609
+ "resolve."
610
+ ),
611
+ )
612
+ @click.option(
613
+ "--select",
614
+ "-s",
615
+ default="*",
616
+ show_default=True,
617
+ help="Selector scoping which metrics the server exposes (metric name, path, or tag)",
618
+ )
619
+ @click.option(
620
+ "--profile",
621
+ help="Profile to use (default: from project config)",
622
+ )
623
+ def mcp(project_dir: str | None, select: str, profile: str | None) -> None:
624
+ """
625
+ Serve a read-only Model Context Protocol (MCP) server over stdio.
626
+
627
+ Exposes a detectkit project to an MCP client (an IDE assistant, a desktop
628
+ app, a cloud agent): metric configs, loaded datapoints, detector results,
629
+ replayed alert history, autotune runs, and labeled incidents. Nothing in
630
+ this server writes to the database or the filesystem — no config edits,
631
+ no label writes, no pipeline runs, no schema creation. It never calls
632
+ `ensure_tables()`; a project with no data yet answers tool calls with a
633
+ clear "run `dtk run` first" error instead of creating tables.
634
+
635
+ `--select` scopes the whole session: every tool that names a metric
636
+ refuses one outside this set, and list/status tools intersect their own
637
+ selector with it. Needs the `[mcp]` extra: pip install 'detectkit[mcp]'.
638
+
639
+ Examples:
640
+ # Expose every metric in the project the current directory is inside
641
+ dtk mcp
642
+
643
+ # Scope to one tag, from a project the client doesn't cd into
644
+ dtk mcp --project-dir /opt/monitoring --select "tag:critical"
645
+ """
646
+ from detectkit.cli.commands.mcp import run_mcp
647
+
648
+ run_mcp(project_dir=project_dir, select=select, profile=profile)
649
+
650
+
595
651
  if __name__ == "__main__":
596
652
  cli()
@@ -176,10 +176,19 @@ class ProfileConfig(BaseModel):
176
176
  else:
177
177
  raise ValueError(f"Unsupported database type: {self.type}")
178
178
 
179
- def create_manager(self) -> BaseDatabaseManager:
179
+ def create_manager(self, ensure_locations: bool = True) -> BaseDatabaseManager:
180
180
  """
181
181
  Create database manager from profile configuration.
182
182
 
183
+ Args:
184
+ ensure_locations: When False, skip creating the internal/data
185
+ database(s)/schema(s) as a side effect of connecting — a
186
+ strict read-only probe. For DuckDB this additionally forces
187
+ a read-only attach (see :class:`DuckDBDatabaseManager`), so
188
+ the connect itself can never create a missing state file.
189
+ Used by the read-only MCP server, which must never run DDL
190
+ at startup.
191
+
183
192
  Returns:
184
193
  Database manager instance
185
194
 
@@ -201,6 +210,7 @@ class ProfileConfig(BaseModel):
201
210
  internal_database=self.get_internal_location(),
202
211
  data_database=self.get_data_location(),
203
212
  settings=self.settings,
213
+ ensure_locations=ensure_locations,
204
214
  )
205
215
  elif self.type == "postgres":
206
216
  from detectkit.database.postgres_manager import PostgresDatabaseManager
@@ -220,6 +230,7 @@ class ProfileConfig(BaseModel):
220
230
  internal_schema=self.get_internal_location(),
221
231
  data_schema=self.get_data_location(),
222
232
  settings=self.settings,
233
+ ensure_locations=ensure_locations,
223
234
  )
224
235
  elif self.type in ("mysql", "mariadb"):
225
236
  # MariaDB is served by the same manager; it detects the vendor at
@@ -236,6 +247,7 @@ class ProfileConfig(BaseModel):
236
247
  internal_database=self.get_internal_location(),
237
248
  data_database=self.get_data_location(),
238
249
  settings=self.settings,
250
+ ensure_locations=ensure_locations,
239
251
  )
240
252
  elif self.type == "duckdb":
241
253
  # In-process, file-backed database: host/port/user/password/database
@@ -256,6 +268,7 @@ class ProfileConfig(BaseModel):
256
268
  data_schema=self.get_data_location(),
257
269
  read_only=self.read_only,
258
270
  settings=self.settings,
271
+ ensure_locations=ensure_locations,
259
272
  )
260
273
  else:
261
274
  raise ValueError(f"Unsupported database type: {self.type}")
@@ -351,18 +364,24 @@ class ProfilesConfig(BaseModel):
351
364
 
352
365
  return self.profiles[name]
353
366
 
354
- def create_manager(self, profile_name: str | None = None) -> BaseDatabaseManager:
367
+ def create_manager(
368
+ self, profile_name: str | None = None, ensure_locations: bool = True
369
+ ) -> BaseDatabaseManager:
355
370
  """
356
371
  Create database manager for a profile.
357
372
 
358
373
  Args:
359
374
  profile_name: Profile name (if None, use default)
375
+ ensure_locations: Passed through to
376
+ :meth:`ProfileConfig.create_manager` — False for a strict
377
+ read-only probe that never runs DDL as a side effect of
378
+ connecting.
360
379
 
361
380
  Returns:
362
381
  Database manager instance
363
382
  """
364
383
  profile = self.get_profile(profile_name)
365
- return profile.create_manager()
384
+ return profile.create_manager(ensure_locations=ensure_locations)
366
385
 
367
386
  def get_alert_channel_config(self, channel_name: str) -> dict[str, Any]:
368
387
  """
@@ -51,6 +51,11 @@ class SQLDatabaseManager(BaseDatabaseManager):
51
51
  database to connect to, inside which the schemas live); optional for
52
52
  MySQL (which addresses tables as ``database.table``).
53
53
  settings: Extra driver-specific connection options.
54
+ ensure_locations: When False, skip the ``_ensure_locations()`` call
55
+ that otherwise runs DDL (``CREATE SCHEMA``/``CREATE DATABASE``)
56
+ as a side effect of connecting — a strict read-only probe (used
57
+ by the read-only MCP server, which must never run DDL at
58
+ startup).
54
59
  """
55
60
 
56
61
  #: Maps the canonical column kind to a native SQL type. Subclasses override.
@@ -69,6 +74,7 @@ class SQLDatabaseManager(BaseDatabaseManager):
69
74
  data_location: str = "public",
70
75
  database: str | None = None,
71
76
  settings: dict[str, Any] | None = None,
77
+ ensure_locations: bool = True,
72
78
  ) -> None:
73
79
  self._host = host
74
80
  self._port = port
@@ -83,7 +89,8 @@ class SQLDatabaseManager(BaseDatabaseManager):
83
89
  self._table_meta: dict[str, tuple[list[str], str | None]] = {}
84
90
 
85
91
  self._conn: Any = self._connect()
86
- self._ensure_locations()
92
+ if ensure_locations:
93
+ self._ensure_locations()
87
94
 
88
95
  # ── dialect hooks (subclasses override) ─────────────────────────────────
89
96
 
@@ -153,7 +160,7 @@ class SQLDatabaseManager(BaseDatabaseManager):
153
160
  if col.default is None:
154
161
  return ""
155
162
  d = col.default
156
- if isinstance(d, (int, float)):
163
+ if isinstance(d, int | float):
157
164
  return f" DEFAULT {d}"
158
165
  if isinstance(d, str) and d.lstrip("-").isdigit():
159
166
  return f" DEFAULT {d}"
@@ -36,6 +36,10 @@ class ClickHouseDatabaseManager(BaseDatabaseManager):
36
36
  internal_database: Database for internal tables (_dtk_*)
37
37
  data_database: Database for user data tables
38
38
  settings: Optional ClickHouse settings
39
+ ensure_locations: When False, skip creating the internal/data
40
+ databases as a side effect of connecting — a strict read-only
41
+ probe (used by the read-only MCP server, which must never run
42
+ DDL at startup).
39
43
  """
40
44
 
41
45
  def __init__(
@@ -47,6 +51,7 @@ class ClickHouseDatabaseManager(BaseDatabaseManager):
47
51
  internal_database: str = "detectk_internal",
48
52
  data_database: str = "default",
49
53
  settings: dict[str, Any] | None = None,
54
+ ensure_locations: bool = True,
50
55
  ):
51
56
  """Initialize ClickHouse manager."""
52
57
  if not CLICKHOUSE_AVAILABLE:
@@ -67,8 +72,9 @@ class ClickHouseDatabaseManager(BaseDatabaseManager):
67
72
  settings=settings or {},
68
73
  )
69
74
 
70
- # Ensure databases exist
71
- self._ensure_databases()
75
+ # Ensure databases exist (skipped for a strict read-only probe).
76
+ if ensure_locations:
77
+ self._ensure_databases()
72
78
 
73
79
  def _ensure_databases(self) -> None:
74
80
  """Create internal and data databases if they don't exist."""
@@ -154,7 +160,7 @@ class ClickHouseDatabaseManager(BaseDatabaseManager):
154
160
  """Format default value for SQL."""
155
161
  if isinstance(value, str):
156
162
  return f"'{value}'"
157
- elif isinstance(value, (int, float)):
163
+ elif isinstance(value, int | float):
158
164
  return str(value)
159
165
  elif value is None:
160
166
  return "NULL"
@@ -228,12 +234,12 @@ class ClickHouseDatabaseManager(BaseDatabaseManager):
228
234
  value = data[col_name][i]
229
235
 
230
236
  # Convert numpy types to Python types
231
- if isinstance(value, (np.datetime64, np.timedelta64)):
237
+ if isinstance(value, np.datetime64 | np.timedelta64):
232
238
  # Convert numpy datetime64 to Python datetime
233
239
  value = self._convert_numpy_datetime(value)
234
240
  elif isinstance(value, np.ndarray):
235
241
  value = value.tolist()
236
- elif isinstance(value, (np.integer, np.floating)):
242
+ elif isinstance(value, np.integer | np.floating):
237
243
  value = value.item()
238
244
  elif value is None or (isinstance(value, float) and np.isnan(value)):
239
245
  value = None
@@ -203,6 +203,18 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
203
203
  internal/data schemas already exist.
204
204
  settings: Extra ``duckdb.connect`` ``config`` options (e.g.
205
205
  ``{"memory_limit": "512MB"}``).
206
+ ensure_locations: When False, skip creating the internal/data
207
+ schemas as a side effect of connecting (a strict read-only
208
+ probe — see
209
+ :class:`~detectkit.database._sql_manager.SQLDatabaseManager`)
210
+ **and** force a read-only attach regardless of ``read_only``,
211
+ for a real file path. Skipping schema creation alone is not
212
+ enough for DuckDB: a plain read-write ``duckdb.connect`` against
213
+ a *missing* file path creates that file as a side effect of
214
+ connecting, before any DDL runs — forcing ``read_only=True``
215
+ is what actually prevents that. ``":memory:"`` is exempted from
216
+ the forced read-only attach (it has no file to create, and
217
+ DuckDB rejects a read-only in-memory connection outright).
206
218
 
207
219
  Raises:
208
220
  ImportError: If the ``duckdb`` package is not installed.
@@ -224,6 +236,7 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
224
236
  data_schema: str = "main",
225
237
  read_only: bool = False,
226
238
  settings: dict[str, Any] | None = None,
239
+ ensure_locations: bool = True,
227
240
  ) -> None:
228
241
  if not DUCKDB_AVAILABLE:
229
242
  raise ImportError(
@@ -235,7 +248,17 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
235
248
  "path, or ':memory:' for a transient, tests/preview-only in-process "
236
249
  "database whose state is lost between runs)."
237
250
  )
238
- self._read_only = read_only
251
+ # `ensure_locations=False` is a strict read-only PROBE (see
252
+ # `SQLDatabaseManager.__init__`): skipping `_ensure_locations()` is
253
+ # not enough on its own for DuckDB, because a plain read-write
254
+ # `duckdb.connect` against a MISSING file creates that file as a
255
+ # side effect of connecting, before any DDL runs. Force a read-only
256
+ # attach so the connect itself can never create the file/schemas.
257
+ # ":memory:" is exempted — it has no file to create in the first
258
+ # place, and DuckDB rejects a read-only in-memory connection outright
259
+ # (`CatalogException: Cannot launch in-memory database in read-only
260
+ # mode!`), so forcing it there would break rather than protect.
261
+ self._read_only = True if (not ensure_locations and path != ":memory:") else read_only
239
262
  # Kept separately (and typed `str`, not `str | None`) from the base
240
263
  # class's `self._database` so `_connect()` doesn't need to narrow an
241
264
  # Optional it knows — by the ValueError check above — can't be None.
@@ -248,6 +271,7 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
248
271
  data_location=data_schema,
249
272
  database=path,
250
273
  settings=settings,
274
+ ensure_locations=ensure_locations,
251
275
  )
252
276
 
253
277
  def _connect(self) -> Any:
@@ -44,6 +44,9 @@ class MySQLDatabaseManager(SQLDatabaseManager):
44
44
  data_database: Database for user data tables.
45
45
  database: Optional default database for the connection.
46
46
  settings: Extra ``pymysql.connect`` keyword arguments.
47
+ ensure_locations: When False, skip creating the internal/data
48
+ databases as a side effect of connecting — a strict read-only
49
+ probe (see :class:`~detectkit.database._sql_manager.SQLDatabaseManager`).
47
50
  """
48
51
 
49
52
  _TYPE_MAP = {
@@ -72,6 +75,7 @@ class MySQLDatabaseManager(SQLDatabaseManager):
72
75
  data_database: str = "analytics",
73
76
  database: str | None = None,
74
77
  settings: dict[str, Any] | None = None,
78
+ ensure_locations: bool = True,
75
79
  ) -> None:
76
80
  if not PYMYSQL_AVAILABLE:
77
81
  raise ImportError(
@@ -86,6 +90,7 @@ class MySQLDatabaseManager(SQLDatabaseManager):
86
90
  data_location=data_database,
87
91
  database=database,
88
92
  settings=settings,
93
+ ensure_locations=ensure_locations,
89
94
  )
90
95
 
91
96
  def _connect(self) -> Any:
@@ -34,6 +34,9 @@ class PostgresDatabaseManager(SQLDatabaseManager):
34
34
  internal_schema: Schema for internal ``_dtk_*`` tables.
35
35
  data_schema: Schema for user data tables.
36
36
  settings: Extra ``psycopg2.connect`` keyword arguments.
37
+ ensure_locations: When False, skip creating the internal/data
38
+ schemas as a side effect of connecting — a strict read-only
39
+ probe (see :class:`~detectkit.database._sql_manager.SQLDatabaseManager`).
37
40
  """
38
41
 
39
42
  _TYPE_MAP = {
@@ -54,6 +57,7 @@ class PostgresDatabaseManager(SQLDatabaseManager):
54
57
  internal_schema: str = "detectkit",
55
58
  data_schema: str = "public",
56
59
  settings: dict[str, Any] | None = None,
60
+ ensure_locations: bool = True,
57
61
  ) -> None:
58
62
  if not PSYCOPG2_AVAILABLE:
59
63
  raise ImportError(
@@ -68,6 +72,7 @@ class PostgresDatabaseManager(SQLDatabaseManager):
68
72
  data_location=data_schema,
69
73
  database=database,
70
74
  settings=settings,
75
+ ensure_locations=ensure_locations,
71
76
  )
72
77
 
73
78
  def _connect(self) -> Any:
@@ -172,7 +172,13 @@ class MetricLoader:
172
172
  # Filter by timestamp
173
173
  row_ts = row[timestamp_col]
174
174
  if isinstance(row_ts, datetime):
175
- # Already datetime - compare directly
175
+ # Naive-UTC convention: a tz-aware timestamp from the source
176
+ # (DuckDB now()/TIMESTAMPTZ, PostgreSQL timestamptz) is
177
+ # converted — not rejected — so it compares cleanly with the
178
+ # naive-UTC window bounds and stores consistently downstream.
179
+ if row_ts.tzinfo is not None:
180
+ row_ts = to_naive_utc(row_ts)
181
+ row[timestamp_col] = row_ts
176
182
  if row_ts >= to_date:
177
183
  continue
178
184
  else:
@@ -0,0 +1,47 @@
1
+ """Read-only Model Context Protocol (MCP) server for detectkit.
2
+
3
+ An **isolated, additive** layer — mirroring ``detectkit/semantic/``'s design:
4
+ nothing in the load/detect/alert pipeline imports it, so it can never affect
5
+ a running project. It powers only the ``dtk mcp`` command
6
+ (:mod:`detectkit.cli.commands.mcp`), which exposes a project's metrics,
7
+ loaded data, detector results, replayed alert history, autotune runs and
8
+ labeled incidents to an MCP client (an IDE assistant, a desktop app, a cloud
9
+ agent) over stdio.
10
+
11
+ The ``mcp`` SDK (the ``[mcp]`` extra, ``pip install 'detectkit[mcp]'``) is a
12
+ guarded, lazy import — see :mod:`detectkit.mcp.server` — so this package (and
13
+ every module in it) imports cleanly even when the extra isn't installed;
14
+ only building/running the actual server needs it.
15
+
16
+ Module map:
17
+
18
+ - ``errors.py`` — :class:`McpError` / :class:`McpProjectError` /
19
+ :class:`McpDependencyMissing`.
20
+ - ``context.py`` — project resolution (``--project-dir`` ->
21
+ ``$DETECTKIT_PROJECT_DIR`` -> cwd search) and the long-lived
22
+ :class:`McpContext` (mirrors ``dtk run``'s build order, minus
23
+ ``ensure_tables()`` — read-only never runs DDL).
24
+ - ``serialize.py`` — ISO-8601 / numpy-free JSON conversion helpers shared by
25
+ every tool.
26
+ - ``tools.py`` — the 10 read-only tool implementations, as plain functions
27
+ (no SDK dependency; directly unit-testable).
28
+ - ``server.py`` — the guarded ``FastMCP`` wiring + ``dtk mcp``'s entry point
29
+ (:func:`~detectkit.mcp.server.run_server`).
30
+
31
+ See :mod:`detectkit.mcp.tools` for the full tool list and the read-only
32
+ exclusion contract (no config/label writes, no pipeline runs, no DDL).
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from detectkit.mcp.context import McpContext, build_context, resolve_project_dir
38
+ from detectkit.mcp.errors import McpDependencyMissing, McpError, McpProjectError
39
+
40
+ __all__ = [
41
+ "McpContext",
42
+ "McpDependencyMissing",
43
+ "McpError",
44
+ "McpProjectError",
45
+ "build_context",
46
+ "resolve_project_dir",
47
+ ]