"""Passenger MCP server — tools, resources, and prompts for the FCE agent only. Covers: passenger notification generation, flight manifest, and notification prompt template (multi-tone). """ import json from fastmcp import FastMCP from mcp_servers.data.models import MPStatus from mcp_servers.data.scenarios.manager import scenario_manager mcp = FastMCP( "stellar-ops-passenger", instructions=( "Passenger-facing tools — notification narrative generation " "and flight manifest access. Restricted to customer-facing clients." ), ) # ── Tools ────────────────────────────────────────────────────────── @mcp.tool() async def generate_notification(context: dict) -> str: """Synthesizes flight disruption context into an empathetic, actionable passenger notification. Uses Claude via Anthropic SDK (or Bedrock when USE_BEDROCK=true). Output: clear, human, no jargon, includes gate/time/status. Falls back to template if no API key is configured. """ try: from mcp_servers.shared_llm import generate, _get_provider system_prompt = ( "You are a passenger notification system for Stellar Air. " "Write a clear, empathetic notification about this flight disruption. " "Explain WHY the delay or cancellation happened using the operational data provided. " "Tell the passenger what's happening next: new boarding time, gate, status. " "Be human and reassuring. No aviation jargon. No speculation. " "If data is missing for a section, omit it — don't make things up." ) text = await generate(system_prompt, json.dumps(context, indent=2)) return json.dumps({"text": text, "provider": _get_provider()}) except Exception: return json.dumps({"text": _template_notification(context), "provider": "template"}) def _template_notification(context: dict) -> str: """Structured template fallback when LLM is unavailable.""" flight_id = context.get("flight_id", "") origin = context.get("origin", "") destination = context.get("destination", "") status = context.get("status", "DELAYED") delay_minutes = context.get("delay_minutes", 0) delay_cause = context.get("delay_cause", "") gate = context.get("gate", "") weather_summary = context.get("weather_summary", "") crew_notes_summary = context.get("crew_notes_summary", "") lines = [ f"{flight_id} — {origin} → {destination}", f"Status: {status}" + (f" {delay_minutes} minutes" if delay_minutes else ""), "", ] if weather_summary: lines.append(f"Your flight is delayed due to {weather_summary}.") elif delay_cause: cause_text = { "WEATHER": "weather conditions along your route", "MAINTENANCE": "a routine maintenance check on your aircraft", "CREW": "ensuring your crew is fully rested for safe operation", "ATC": "air traffic control restrictions", "LATE_AIRCRAFT": "the late arrival of your inbound aircraft", }.get(delay_cause, f"{delay_cause.lower()}") lines.append(f"Your flight is delayed due to {cause_text}.") if crew_notes_summary: lines.append(crew_notes_summary) if gate: lines.append(f"\nGate {gate} — no gate change expected.") return "\n".join(lines) # ── Resources ────────────────────────────────────────────────────── @mcp.resource("ops://flights/{flight_id}/manifest") def flight_manifest(flight_id: str) -> str: """Passenger manifest summary for a flight. Returns: flight_id, passenger count, count by MP status tier, special needs count, connection count. Summary-level — no PII exposed through this resource. """ flight = None for f in scenario_manager.flights: if f.flight_id == flight_id: flight = f break if not flight: return json.dumps({"error": f"Flight {flight_id} not found"}) # Count passengers by status from scenario data pax_on_flight = [ p for p in scenario_manager.passengers if p.flight_id == flight_id ] status_counts = {} special_needs_count = 0 connection_count = 0 for p in pax_on_flight: status = p.mileage_plus_status.value status_counts[status] = status_counts.get(status, 0) + 1 if p.special_needs: special_needs_count += 1 if p.connection_flight: connection_count += 1 return json.dumps({ "flight_id": flight_id, "total_passengers": flight.passenger_count, "by_status": status_counts, "special_needs_count": special_needs_count, "connection_count": connection_count, }) # ── Prompts ──────────────────────────────────────────────────────── TONE_INSTRUCTIONS = { "empathetic": ( "Write a passenger notification about this flight disruption. " "Be empathetic and human. Explain WHY the delay/cancellation happened " "using the weather, maintenance, or operational data provided. " "Tell the passenger what's happening next: new boarding time, gate, " "rebooking options. End with reassurance. No jargon." ), "factual": ( "Write a brief, factual notification. Include: flight number, " "route, status, delay duration, cause (one phrase), new departure time, " "gate. No editorial. No reassurance. Just facts." ), "brief_sms": ( "Write an SMS-length notification (under 160 characters). " "Format: UA{flight} {route}: {status}. {cause}. New dep: {time}. Gate {gate}." ), } @mcp.prompt() def passenger_notification(tone: str = "empathetic") -> str: """Template for generating passenger delay/cancellation notifications. tone: empathetic (default) | factual | brief_sms """ instruction = TONE_INSTRUCTIONS.get(tone, TONE_INSTRUCTIONS["empathetic"]) return ( f"{instruction}\n\n" "Use the operational data provided below. Do not invent details. " "If data is missing for a section, omit that section." )