Skip to content

Commit 1c71604

Browse files
author
ci bot
committed
Merge branch 'feat/TG-1090-mcp-monitors-l1' into 'enterprise'
feat(mcp): add monitor read tools — group summary and per-table inventory (TG-1090) See merge request dkinternal/testgen/dataops-testgen!539
2 parents b11eaae + 2de9b0b commit 1c71604

8 files changed

Lines changed: 1596 additions & 245 deletions

File tree

testgen/common/enums.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,12 @@ class PiiRisk(StrEnum):
102102
"""Risk level extracted from PII issue ``detail`` strings via ``priority`` hybrid."""
103103
HIGH = "High"
104104
MODERATE = "Moderate"
105+
106+
107+
class MonitorType(StrEnum):
108+
"""Stored ``test_type`` values for the four monitor test types. Surfaced to users
109+
as the lowercase short labels (freshness / volume / schema / metric)."""
110+
FRESHNESS = "Freshness_Trend"
111+
VOLUME = "Volume_Trend"
112+
SCHEMA = "Schema_Drift"
113+
METRIC = "Metric_Trend"

testgen/common/models/table_group.py

Lines changed: 430 additions & 0 deletions
Large diffs are not rendered by default.

testgen/mcp/server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def build_mcp_server(
172172
search_hygiene_issues,
173173
update_hygiene_issue,
174174
)
175+
from testgen.mcp.tools.monitors import get_monitor_summary, list_monitored_tables
175176
from testgen.mcp.tools.notifications import (
176177
create_notification,
177178
delete_notification,
@@ -316,6 +317,8 @@ def safe_prompt(fn):
316317
safe_tool(compare_profiling_runs)
317318
safe_tool(get_profiling_trends)
318319
safe_tool(get_schema_history)
320+
safe_tool(get_monitor_summary)
321+
safe_tool(list_monitored_tables)
319322
safe_tool(run_tests)
320323
safe_tool(run_profiling)
321324
safe_tool(cancel_test_run)

testgen/mcp/tools/common.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
from sqlalchemy import select
77

88
from testgen.common.date_service import parse_since
9-
from testgen.common.enums import Disposition, ImpactDimension, IssueLikelihood, JobStatus, PiiRisk, QualityDimension
9+
from testgen.common.enums import (
10+
Disposition,
11+
ImpactDimension,
12+
IssueLikelihood,
13+
JobStatus,
14+
MonitorType,
15+
PiiRisk,
16+
QualityDimension,
17+
)
1018
from testgen.common.flavors import FLAVOR_CODE_TO_FAMILY, FLAVOR_CODE_TO_LABEL, SqlFlavorLabel
1119
from testgen.common.models import get_current_session
1220
from testgen.common.models.connection import Connection
@@ -57,6 +65,7 @@ class DocGroup(StrEnum):
5765
DISCOVER = "Discover what TestGen knows about"
5866
INVESTIGATE = "Investigate quality issues"
5967
BROWSE_PROFILING = "Browse profiling results"
68+
MONITORS = "Browse monitor health and events"
6069
TRIGGER = "Trigger profiling, tests, and test generation"
6170
SCORING = "Track data quality scores"
6271
MANAGE = "Manage TestGen configuration"
@@ -340,6 +349,51 @@ def next_scheduled_run(
340349
return min(s.get_sample_triggering_timestamps(1)[0] for s in schedules)
341350

342351

352+
# Monitor type — internal ``test_type`` value ↔ user-facing short label (the form
353+
# callers pass and the form rendered in output).
354+
_MONITOR_TYPE_USER_TO_DB: dict[str, MonitorType] = {
355+
"freshness": MonitorType.FRESHNESS,
356+
"volume": MonitorType.VOLUME,
357+
"schema": MonitorType.SCHEMA,
358+
"metric": MonitorType.METRIC,
359+
}
360+
361+
362+
def parse_monitor_type(value: str, label: str = "monitor_type") -> MonitorType:
363+
"""Validate a user-facing monitor type label and return the stored ``MonitorType``.
364+
365+
Accepts ``freshness`` / ``volume`` / ``schema`` / ``metric``. ``label`` names the
366+
caller's argument in the error message — pass ``"anomaly_type"`` when the
367+
public arg is named differently from ``monitor_type``.
368+
"""
369+
db_value = _MONITOR_TYPE_USER_TO_DB.get(value)
370+
if db_value is None:
371+
valid = ", ".join(_MONITOR_TYPE_USER_TO_DB)
372+
raise MCPUserError(f"Invalid {label} `{value}`. Valid values: {valid}")
373+
return db_value
374+
375+
376+
class MonitorTableSort(StrEnum):
377+
"""User-facing values accepted for the ``sort_by`` argument on ``list_monitored_tables``.
378+
379+
When an ``anomaly_type`` filter is set, ``anomaly_count_desc`` sorts by that type's
380+
count; with no filter, it sorts by total anomalies across all types.
381+
"""
382+
383+
TABLE_NAME = "table_name"
384+
ANOMALY_COUNT_DESC = "anomaly_count_desc"
385+
LATEST_UPDATE_DESC = "latest_update_desc"
386+
ROW_COUNT_CHANGE_DESC = "row_count_change_desc"
387+
388+
389+
def parse_monitor_table_sort(value: str) -> MonitorTableSort:
390+
try:
391+
return MonitorTableSort(value)
392+
except ValueError as err:
393+
valid = ", ".join(s.value for s in MonitorTableSort)
394+
raise MCPUserError(f"Invalid sort_by `{value}`. Valid values: {valid}") from err
395+
396+
343397
def parse_disposition(value: str) -> Disposition:
344398
"""Validate a user-facing disposition label and return the stored ``Disposition``.
345399
@@ -529,10 +583,17 @@ def resolve_issue_type(name: str) -> str:
529583

530584

531585
def format_page_info(total: int, page: int, limit: int) -> str:
532-
"""Shared pagination summary line for MCP tool output."""
586+
"""Shared pagination summary line for MCP tool output.
587+
588+
Returns empty for a zero total *or* when ``page`` is past the last page \u2014
589+
the caller's own "no rows on page N" message is more useful than a
590+
``Showing 6\u20135 of 5`` nonsense range.
591+
"""
533592
if total == 0:
534593
return ""
535594
start = (page - 1) * limit + 1
595+
if start > total:
596+
return ""
536597
end = min(start + limit - 1, total)
537598
return f"Showing {start}\u2013{end} of {total} (page {page})."
538599

@@ -595,6 +656,23 @@ def resolve_test_suite(test_suite_id: str) -> TestSuite:
595656
return suite
596657

597658

659+
def resolve_monitored_table_group(table_group_id: str) -> tuple[TableGroup, TestSuite | None]:
660+
"""Resolve a table group ID and look up its linked monitor suite.
661+
662+
Returns ``(table_group, monitor_suite)``. ``monitor_suite`` is ``None`` when the
663+
table group has no ``monitor_test_suite_id`` set, or that pointer doesn't resolve
664+
to an ``is_monitor=True`` suite — callers render the "not monitored" output in
665+
that case rather than raising an inaccessible error. Raises
666+
``MCPResourceNotAccessible`` when the table group itself is missing or out of
667+
scope.
668+
"""
669+
tg = resolve_table_group(table_group_id)
670+
if tg.monitor_test_suite_id is None:
671+
return tg, None
672+
suite = TestSuite.get(tg.monitor_test_suite_id, TestSuite.is_monitor.is_(True))
673+
return tg, suite
674+
675+
598676
def resolve_profiling_run(job_execution_id: str) -> ProfilingRun:
599677
"""Resolve a profiling run by id-or-JE-id, scoped to allowed projects.
600678

0 commit comments

Comments
 (0)