Skip to main content
Glama
serviceDisruptions.ts8.94 kB
import { DynamicLineService } from './dynamicLineService.js'; import { StationMapper } from './stationMapper.js'; export interface DisruptionInfo { line: string; severity: 'CRITICAL' | 'MAJOR' | 'MINOR' | 'PLANNED'; category: 'DELAYS' | 'SUSPENSIONS' | 'REROUTES' | 'PLANNED_WORK' | 'ACCESSIBILITY'; title: string; description: string; affectedStations: string[]; estimatedResolution?: string; alternativeRoutes: string[]; impact: 'system_wide' | 'line_wide' | 'local'; } export class ServiceDisruptionAnalyzer { private static stationMapper = new StationMapper(); static async analyze(params: any, alertsData: any, statusData: any): Promise<any> { const serviceContext = DynamicLineService.getServiceContext(); const disruptions = await this.analyzeDisruptions(alertsData, statusData, params); return this.formatDisruptionResponse(disruptions, serviceContext, params); } private static async analyzeDisruptions(alertsData: any, statusData: any, params: any): Promise<DisruptionInfo[]> { const alerts = alertsData.entity?.filter((entity: any) => entity.alert) || []; const analyzedDisruptions: DisruptionInfo[] = []; for (const entity of alerts) { const alert = entity.alert; if (!alert) continue; // Extract affected lines const affectedLines = alert.informedEntity?.map((ie: any) => ie.routeId).filter(Boolean) || []; // Extract affected stations and convert to human-readable names const affectedStopIds = alert.informedEntity?.map((ie: any) => ie.stopId).filter(Boolean) || []; const affectedStations: string[] = []; for (const stopId of affectedStopIds) { const stationName = await this.stationMapper.getStationName(stopId); affectedStations.push(stationName); } // Determine severity based on alert effect let severity: DisruptionInfo['severity'] = 'MINOR'; if (alert.effect === 'NO_SERVICE') severity = 'CRITICAL'; else if (alert.effect === 'REDUCED_SERVICE' || alert.effect === 'SIGNIFICANT_DELAYS') severity = 'MAJOR'; else if (alert.effect === 'DETOUR' || alert.effect === 'ADDITIONAL_SERVICE') severity = 'MINOR'; // Determine category based on alert content let category: DisruptionInfo['category'] = 'DELAYS'; const headerText = alert.headerText?.translation?.[0]?.text?.toLowerCase() || ''; const descText = alert.descriptionText?.translation?.[0]?.text?.toLowerCase() || ''; const combinedText = `${headerText} ${descText}`; if (combinedText.includes('suspend') || combinedText.includes('no service')) { category = 'SUSPENSIONS'; } else if (combinedText.includes('reroute') || combinedText.includes('detour')) { category = 'REROUTES'; } else if (combinedText.includes('planned') || combinedText.includes('weekend') || combinedText.includes('maintenance')) { category = 'PLANNED_WORK'; severity = 'PLANNED'; } else if (combinedText.includes('elevator') || combinedText.includes('escalator') || combinedText.includes('accessible')) { category = 'ACCESSIBILITY'; } // Determine impact scope let impact: DisruptionInfo['impact'] = 'local'; if (affectedLines.length > 3) impact = 'system_wide'; else if (affectedLines.length > 1) impact = 'line_wide'; // Extract alternative routes from alert text only const alternativeRoutes = this.extractAlternativeRoutes(alert); // Create disruption info for each affected line for (const line of affectedLines) { analyzedDisruptions.push({ line, severity, category, title: alert.headerText?.translation?.[0]?.text || 'Service Alert', description: alert.descriptionText?.translation?.[0]?.text || 'No details available', affectedStations, estimatedResolution: this.extractResolutionTime(alert), alternativeRoutes, impact }); } } // Apply filters return analyzedDisruptions.filter(disruption => { if (params.line && disruption.line !== params.line) return false; if (params.severity && params.severity !== 'ALL' && disruption.severity !== params.severity) return false; if (params.location) { const locationLower = params.location.toLowerCase(); return disruption.affectedStations.some(station => station.toLowerCase().includes(locationLower) ) || disruption.description.toLowerCase().includes(locationLower); } return true; }); } private static extractAlternativeRoutes(alert: any): string[] { const alternatives: string[] = []; const description = alert.descriptionText?.translation?.[0]?.text || ''; // Only extract alternatives explicitly mentioned in MTA alert text const altRoutePatterns = [ /use\s+([A-Z0-9,\s]+)\s+train/gi, /take\s+([A-Z0-9,\s]+)\s+train/gi, /consider\s+([A-Z0-9,\s]+)\s+train/gi, /transfer\s+to\s+([A-Z0-9,\s]+)/gi ]; altRoutePatterns.forEach(pattern => { const matches = description.match(pattern); if (matches) { matches.forEach((match: string) => { const routes = match.replace(/(use|take|consider|transfer to)\s+|train/gi, '').trim(); if (routes) { alternatives.push(`Use ${routes} trains`); } }); } }); return alternatives; } private static extractResolutionTime(alert: any): string | undefined { const description = alert.descriptionText?.translation?.[0]?.text || ''; // Look for time patterns const timePatterns = [ /until\s+(\d{1,2}:\d{2}\s*[AP]M)/i, /through\s+(\w+day)/i, /(\d{1,2}\/\d{1,2})/, /until\s+further\s+notice/i ]; for (const pattern of timePatterns) { const match = description.match(pattern); if (match) { return match[1] || match[0]; } } return undefined; } private static formatDisruptionResponse( disruptions: DisruptionInfo[], serviceContext: any, params: any ): any { if (disruptions.length === 0) { let response = `✅ **No major service disruptions detected**\n\n`; if (params.line) { response += `The ${params.line} train appears to be running normally.\n\n`; } else { response += `NYC subway system operating normally.\n\n`; } if (serviceContext.serviceNote) { response += `${serviceContext.serviceNote}\n\n`; } response += `💡 **Tips:**\n`; response += `• Check "next trains at [station]" for real-time arrivals\n`; response += `• Minor delays may still occur during peak hours\n`; response += `• Weekend service patterns may differ\n\n`; response += `⚠️ Data provided "as is" without warranty. Source: MTA. Not endorsed by MTA.`; return { content: [{ type: "text", text: response }] }; } let response = `🚨 **Service Disruptions Detected**\n\n`; const critical = disruptions.filter(d => d.severity === 'CRITICAL'); const major = disruptions.filter(d => d.severity === 'MAJOR'); const minor = disruptions.filter(d => d.severity === 'MINOR'); if (critical.length > 0) { response += `🔴 **CRITICAL DISRUPTIONS:**\n`; critical.forEach(d => { response += `**${d.line} Train:** ${d.title}\n`; response += `${d.description}\n`; if (d.estimatedResolution) { response += `⏱️ Estimated resolution: ${d.estimatedResolution}\n`; } response += `🔄 **Alternatives:** ${d.alternativeRoutes.join(', ')}\n\n`; }); } if (major.length > 0) { response += `🟡 **MAJOR DISRUPTIONS:**\n`; major.forEach(d => { response += `**${d.line} Train:** ${d.title}\n`; response += `${d.description}\n`; if (d.estimatedResolution) { response += `⏱️ Estimated resolution: ${d.estimatedResolution}\n`; } response += `🔄 **Alternatives:** ${d.alternativeRoutes.join(', ')}\n\n`; }); } if (minor.length > 0) { response += `🟢 **MINOR DISRUPTIONS:**\n`; minor.forEach(d => { response += `**${d.line} Train:** ${d.title} - ${d.description}\n`; }); response += `\n`; } if (serviceContext.serviceNote) { response += `${serviceContext.serviceNote}\n\n`; } response += `💡 **Recommendations:**\n`; response += `• Allow extra travel time\n`; response += `• Check real-time arrivals before traveling\n`; response += `• Consider alternative routes listed above\n\n`; response += `⚠️ Data provided "as is" without warranty. Source: MTA. Not endorsed by MTA.`; return { content: [{ type: "text", text: response }] }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sasabasara/where_is_my_train_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server