""" MCP tool for executing PromQL range queries. This tool allows authorized AI agents and developers to perform historical time-series data analysis and trend evaluation. Returns JSON in Prometheus API format: {"status": "success", "data": {"resultType": "matrix", "result": [...], "warnings": []}}. """ import json from typing import Any from fastmcp import FastMCP from pydantic import ValidationError as PydanticValidationError from src.models.query_range import RangeQueryRequest from src.services.auth import get_valid_server_auth from src.services.prometheus import get_prometheus_client_for_cluster from src.tools.constants import INSTANT_QUERY_TIMEOUT from src.utils.errors import ( AuthorizationError, AuthServiceError, NetworkError, PrometheusAPIError, PrometheusTimeoutError, ValidationError, ) from src.utils.logging_utils import get_logger from src.utils.redaction import redact_labels from src.utils.tool_response import format_tool_error, tool_text_response # Initialize MCP server instance (will be imported by server.py) mcp = FastMCP("TiDB Observability Server") logger = get_logger("tool.query_range") @mcp.tool() async def query_range( query: str, start: str, end: str, step: str, cluster_id: str, env: str, deploy_type: str, ) -> list[dict[str, Any]]: """ Execute a PromQL range query and return time-series data points. WORKFLOW: `list_metrics` -> `list_metric_labels` -> `list_metric_label_values` -> `query_range` for metric data. Returns Prometheus /api/v1/query_range JSON data. """ try: logger.info( "tool_call_start name=query_range cluster_id=%s env=%s deploy_type=%s", cluster_id, env, deploy_type, ) await get_valid_server_auth() try: request = RangeQueryRequest.model_validate( { "query": query, "start": start, "end": end, "step": step, "cluster_id": str(cluster_id).strip(), "env": env.strip(), "deploy_type": deploy_type.strip(), } ) except (ValueError, PydanticValidationError) as e: raise ValidationError(f"Invalid range query parameters: {str(e)}") from e client = await get_prometheus_client_for_cluster( cluster_id=request.cluster_id, env=request.env, deploy_type=request.deploy_type, timeout=INSTANT_QUERY_TIMEOUT, ) response = await client.query_range( query=request.query, start=request.start, end=request.end, step=request.step, ) # Redact sensitive labels in each series; keep Prometheus response structure result_data = response.get("data", {}).get("result", []) for series in result_data: metric = series.get("metric", {}) if metric: series["metric"] = redact_labels(metric) logger.info( "tool_call_success name=query_range cluster_id=%s series_count=%s", request.cluster_id, len(result_data), ) return tool_text_response(json.dumps(response, ensure_ascii=False)) except ( AuthServiceError, AuthorizationError, ValidationError, NetworkError, PrometheusTimeoutError, PrometheusAPIError, ) as e: logger.warning("tool_call_error name=query_range type=%s", e.__class__.__name__) return tool_text_response(format_tool_error(e)) except Exception as e: logger.exception("tool_call_unexpected_error name=query_range") return tool_text_response(format_tool_error(e))