M13: harden MCP trust boundary

This commit is contained in:
NuklearRabbit
2026-08-10 03:24:17 +02:00
parent 15bdbe40ac
commit 218599af7d
6 changed files with 115 additions and 23 deletions
+38 -14
View File
@@ -3,11 +3,11 @@ from __future__ import annotations
import uuid
from datetime import date
from fastapi import APIRouter, Depends, Header, Query
from fastapi import APIRouter, Depends, Header, Query, Response
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.deps import get_db, require_mcp_service_token
from app.api.deps import McpClientContext, 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
@@ -42,7 +42,8 @@ def get_correlation_id(
def _audit_service_request(
db: Session, *, client_id: str, tool: str, status_label: str, correlation_id: str
db: Session, *, client_id: str, tool: str, status_label: str, correlation_id: str,
metadata: dict[str, object] | None = None,
) -> None:
record_audit_event(
db,
@@ -51,43 +52,53 @@ def _audit_service_request(
action="mcp_tool_request",
entity_type="mcp_tool",
correlation_id=uuid.UUID(correlation_id),
metadata={"tool": tool, "status": status_label},
metadata={"tool": tool, "status": status_label, **(metadata or {})},
)
db.commit()
def _set_trace_headers(response: Response, correlation_id: str, tenant: str) -> None:
response.headers["X-Correlation-Id"] = correlation_id
response.headers["X-Tenant-Id"] = tenant
response.headers["Cache-Control"] = "no-store"
@router.get("/operations-summary", response_model=OperationsSummaryOut)
def operations_summary(
response: Response,
db: Session = Depends(get_db),
client_id: str = Depends(require_mcp_service_token),
client: McpClientContext = Depends(require_mcp_service_token),
correlation_id: str = Depends(get_correlation_id),
) -> OperationsSummaryOut:
metrics = compute_metrics(db)
_set_trace_headers(response, correlation_id, client.tenant)
_audit_service_request(
db,
client_id=client_id,
client_id=client.client_id,
tool="fleet_ops_get_operations_summary",
status_label="ok",
correlation_id=correlation_id,
)
return OperationsSummaryOut(tenant=settings.ragcore_tenant, metrics=metrics)
return OperationsSummaryOut(tenant=client.tenant, metrics=metrics)
@router.get("/attention-vehicles", response_model=list[AttentionVehicleOut])
def attention_vehicles(
response: Response,
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),
client: McpClientContext = Depends(require_mcp_service_token),
correlation_id: str = Depends(get_correlation_id),
) -> list[AttentionVehicleOut]:
results = list_attention_vehicles(
db, minimum_severity=minimum_severity, on_or_before=date_filter, limit=limit
)
_set_trace_headers(response, correlation_id, client.tenant)
_audit_service_request(
db,
client_id=client_id,
client_id=client.client_id,
tool="fleet_ops_list_attention_vehicles",
status_label="ok",
correlation_id=correlation_id,
@@ -98,15 +109,17 @@ def attention_vehicles(
@router.get("/vehicles/{vehicle_ref}", response_model=McpVehicleDetailOut)
def vehicle_details(
vehicle_ref: str,
response: Response,
db: Session = Depends(get_db),
client_id: str = Depends(require_mcp_service_token),
client: McpClientContext = Depends(require_mcp_service_token),
correlation_id: str = Depends(get_correlation_id),
) -> McpVehicleDetailOut:
_set_trace_headers(response, correlation_id, client.tenant)
vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == vehicle_ref))
if vehicle is None:
_audit_service_request(
db,
client_id=client_id,
client_id=client.client_id,
tool="fleet_ops_get_vehicle_details",
status_label="not_found",
correlation_id=correlation_id,
@@ -128,7 +141,7 @@ def vehicle_details(
_audit_service_request(
db,
client_id=client_id,
client_id=client.client_id,
tool="fleet_ops_get_vehicle_details",
status_label="ok",
correlation_id=correlation_id,
@@ -150,18 +163,29 @@ def vehicle_details(
@router.post("/search-knowledge", response_model=GroundedAnswer)
def search_knowledge(
body: McpKnowledgeSearchRequest,
response: Response,
db: Session = Depends(get_db),
client_id: str = Depends(require_mcp_service_token),
client: McpClientContext = Depends(require_mcp_service_token),
correlation_id: str = Depends(get_correlation_id),
) -> GroundedAnswer:
provider = get_knowledge_provider()
answer = provider.ask(body.question, correlation_id, language=body.locale)
source_count_available = len(answer.sources)
answer.sources = answer.sources[: body.max_sources]
_set_trace_headers(response, correlation_id, client.tenant)
response.headers["X-Sources-Available"] = str(source_count_available)
response.headers["X-Sources-Returned"] = str(len(answer.sources))
_audit_service_request(
db,
client_id=client_id,
client_id=client.client_id,
tool="fleet_ops_search_knowledge",
status_label=answer.evidence_state,
correlation_id=correlation_id,
metadata={
"tenant": client.tenant,
"locale": body.locale,
"sources_available": source_count_available,
"sources_returned": len(answer.sources),
},
)
return answer