from __future__ import annotations import uuid from datetime import date from fastapi import APIRouter, Depends, Query from sqlalchemy import select from sqlalchemy.orm import Session from app.api.deps import get_db, require_mcp_service_token from app.core.config import get_settings from app.core.errors import AppError from app.models.booking import Booking from app.models.data_quality import DataQualityIssue from app.models.vehicle import Vehicle from app.schemas import ( AttentionVehicleOut, McpKnowledgeSearchRequest, McpVehicleDetailOut, OperationsSummaryOut, ) from app.services.audit import record_audit_event from app.services.knowledge import GroundedAnswer, get_knowledge_provider from app.services.operations import compute_metrics, list_attention_vehicles router = APIRouter(prefix="/api/v1/integrations/mcp", tags=["mcp"]) settings = get_settings() def _audit_service_request(db: Session, *, client_id: str, tool: str, status_label: str) -> None: record_audit_event( db, actor_type="service", actor_label=client_id, action="mcp_tool_request", entity_type="mcp_tool", correlation_id=uuid.uuid4(), metadata={"tool": tool, "status": status_label}, ) db.commit() @router.get("/operations-summary", response_model=OperationsSummaryOut) def operations_summary( db: Session = Depends(get_db), client_id: str = Depends(require_mcp_service_token), ) -> OperationsSummaryOut: metrics = compute_metrics(db) _audit_service_request( db, client_id=client_id, tool="mobilityops_get_operations_summary", status_label="ok" ) return OperationsSummaryOut(tenant=settings.ragcore_tenant, metrics=metrics) @router.get("/attention-vehicles", response_model=list[AttentionVehicleOut]) def attention_vehicles( minimum_severity: str = Query(default="medium", pattern="^(low|medium|high)$"), date_filter: date | None = Query(default=None, alias="date"), limit: int = Query(default=20, ge=1, le=50), db: Session = Depends(get_db), client_id: str = Depends(require_mcp_service_token), ) -> list[AttentionVehicleOut]: results = list_attention_vehicles( db, minimum_severity=minimum_severity, on_or_before=date_filter, limit=limit ) _audit_service_request( db, client_id=client_id, tool="mobilityops_list_attention_vehicles", status_label="ok" ) return [AttentionVehicleOut(**r) for r in results] @router.get("/vehicles/{vehicle_ref}", response_model=McpVehicleDetailOut) def vehicle_details( vehicle_ref: str, db: Session = Depends(get_db), client_id: str = Depends(require_mcp_service_token), ) -> McpVehicleDetailOut: vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == vehicle_ref)) if vehicle is None: _audit_service_request( db, client_id=client_id, tool="mobilityops_get_vehicle_details", status_label="not_found", ) raise AppError("VEHICLE_NOT_FOUND", "Vehicle not found.", status_code=404) open_issue_count = len( db.scalars( select(DataQualityIssue.id).where( DataQualityIssue.entity_type == "vehicle", DataQualityIssue.entity_id == vehicle.id, DataQualityIssue.status == "open", ) ).all() ) current_booking = db.scalar( select(Booking).where(Booking.vehicle_id == vehicle.id, Booking.status == "active") ) _audit_service_request( db, client_id=client_id, tool="mobilityops_get_vehicle_details", status_label="ok" ) return McpVehicleDetailOut( public_ref=vehicle.public_ref, make=vehicle.make, model=vehicle.model, model_year=vehicle.model_year, location=vehicle.location, operational_status=vehicle.operational_status, odometer_km=vehicle.odometer_km, next_service_km=vehicle.next_service_km, open_quality_issue_count=open_issue_count, current_booking_ref=current_booking.public_ref if current_booking else None, ) @router.post("/search-knowledge", response_model=GroundedAnswer) def search_knowledge( body: McpKnowledgeSearchRequest, db: Session = Depends(get_db), client_id: str = Depends(require_mcp_service_token), ) -> GroundedAnswer: provider = get_knowledge_provider() correlation_id = str(uuid.uuid4()) answer = provider.ask(body.question, correlation_id) answer.sources = answer.sources[: body.max_sources] _audit_service_request( db, client_id=client_id, tool="mobilityops_search_knowledge", status_label=answer.evidence_state, ) return answer