@siva-sub/mcp-public-transport
Version:
A Model Context Protocol server for Singapore transport data with real-time information and routing
298 lines (297 loc) • 13.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BusStopDetailsTool = void 0;
const zod_1 = require("zod");
const base_js_1 = require("../base.js");
const logger_js_1 = require("../../utils/logger.js");
const haversine_distance_1 = __importDefault(require("haversine-distance"));
const BusStopDetailsInputSchema = zod_1.z.object({
busStopCode: zod_1.z.string().min(5, 'Bus stop code must be at least 5 characters'),
includeServices: zod_1.z.boolean().default(true),
includeNearbyStops: zod_1.z.boolean().default(true),
includeNearbyAmenities: zod_1.z.boolean().default(false),
nearbyRadius: zod_1.z.number().min(50).max(1000).default(200),
});
class BusStopDetailsTool extends base_js_1.BaseTool {
constructor(ltaService, oneMapService) {
super();
this.ltaService = ltaService;
this.oneMapService = oneMapService;
}
getDefinitions() {
return [
{
name: 'get_bus_stop_details',
description: 'Get comprehensive details about a specific bus stop including services, nearby amenities, accessibility information, and location context.',
inputSchema: this.createSchema({
busStopCode: {
type: 'string',
description: 'Bus stop code (e.g., "83139", "01012")',
minLength: 5,
},
includeServices: {
type: 'boolean',
default: true,
description: 'Include bus services and real-time arrival information',
},
includeNearbyStops: {
type: 'boolean',
default: true,
description: 'Include nearby bus stops within walking distance',
},
includeNearbyAmenities: {
type: 'boolean',
default: false,
description: 'Include nearby amenities like shopping centers, food courts, etc.',
},
nearbyRadius: {
type: 'number',
minimum: 50,
maximum: 1000,
default: 200,
description: 'Search radius in meters for nearby stops and amenities',
},
}, ['busStopCode']),
},
];
}
canHandle(toolName) {
return toolName === 'get_bus_stop_details';
}
async execute(toolName, args) {
try {
const { busStopCode, includeServices, includeNearbyStops, includeNearbyAmenities, nearbyRadius, } = BusStopDetailsInputSchema.parse(args);
logger_js_1.logger.info(`Getting details for bus stop: ${busStopCode}`, {
includeServices,
includeNearbyStops,
includeNearbyAmenities,
nearbyRadius,
});
// Get all bus stops to find the target stop
const allBusStops = await this.ltaService.getAllBusStops();
const targetStop = allBusStops.find(stop => stop.busStopCode === busStopCode);
if (!targetStop) {
return {
error: `Bus stop ${busStopCode} not found`,
suggestions: [
'Verify the bus stop code is correct',
'Try using the search_bus_stops tool to find the correct code',
],
timestamp: new Date().toISOString(),
};
}
// Build the detailed response
const details = {
busStopCode: targetStop.busStopCode,
description: targetStop.description,
roadName: targetStop.roadName,
coordinates: {
latitude: targetStop.latitude,
longitude: targetStop.longitude,
},
services: [],
nearbyStops: [],
nearbyAmenities: [],
accessibility: this.inferAccessibilityInfo(targetStop),
locationContext: await this.getLocationContext(targetStop),
operationalInfo: {
operatingHours: '24/7 (service dependent)',
lastUpdated: new Date().toISOString(),
dataSource: 'LTA DataMall',
},
};
// Get bus services if requested
if (includeServices) {
try {
details.services = await this.ltaService.getBusArrival(busStopCode);
}
catch (error) {
logger_js_1.logger.warn(`Failed to get bus services for ${busStopCode}`, error);
details.services = [];
}
}
// Get nearby stops if requested
if (includeNearbyStops) {
details.nearbyStops = await this.getNearbyStops(targetStop, allBusStops, nearbyRadius);
}
// Get nearby amenities if requested
if (includeNearbyAmenities) {
details.nearbyAmenities = await this.getNearbyAmenities(targetStop, nearbyRadius);
}
return {
success: true,
details,
metadata: {
servicesCount: details.services.length,
nearbyStopsCount: details.nearbyStops.length,
nearbyAmenitiesCount: details.nearbyAmenities.length,
searchRadius: nearbyRadius,
},
timestamp: new Date().toISOString(),
};
}
catch (error) {
logger_js_1.logger.error(`Bus stop details failed: ${toolName}`, error);
return this.formatError(error, toolName);
}
}
async getNearbyStops(targetStop, allStops, radius) {
const nearbyStops = allStops
.filter(stop => stop.busStopCode !== targetStop.busStopCode)
.map(stop => ({
...stop,
distance: (0, haversine_distance_1.default)({ latitude: targetStop.latitude, longitude: targetStop.longitude }, { latitude: stop.latitude, longitude: stop.longitude }),
}))
.filter(stop => stop.distance <= radius)
.sort((a, b) => a.distance - b.distance)
.slice(0, 10); // Limit to 10 nearby stops
// Get service counts for nearby stops
const stopsWithServices = await Promise.all(nearbyStops.map(async (stop) => {
try {
const services = await this.ltaService.getBusArrival(stop.busStopCode);
return {
busStopCode: stop.busStopCode,
description: stop.description,
distance: Math.round(stop.distance),
walkingTime: Math.ceil(stop.distance / 80), // 80m/min walking speed
servicesCount: services.length,
};
}
catch (error) {
return {
busStopCode: stop.busStopCode,
description: stop.description,
distance: Math.round(stop.distance),
walkingTime: Math.ceil(stop.distance / 80),
servicesCount: 0,
};
}
}));
return stopsWithServices;
}
async getNearbyAmenities(targetStop, radius) {
const amenities = [];
try {
// Search for nearby amenities using OneMap
const searchQueries = [
'shopping mall',
'hawker centre',
'food court',
'mrt station',
'hospital',
'school',
'community centre',
'park',
];
for (const query of searchQueries) {
try {
const results = await this.oneMapService.geocodeWithAuth(`${query} near ${targetStop.roadName}`);
for (const result of results.slice(0, 2)) { // Limit results per category
const distance = (0, haversine_distance_1.default)({ latitude: targetStop.latitude, longitude: targetStop.longitude }, { latitude: parseFloat(result.LATITUDE), longitude: parseFloat(result.LONGITUDE) });
if (distance <= radius) {
amenities.push({
name: result.BUILDING || result.ROAD_NAME || 'Unknown',
type: this.categorizeAmenity(query),
distance: Math.round(distance),
walkingTime: Math.ceil(distance / 80),
coordinates: {
latitude: parseFloat(result.LATITUDE),
longitude: parseFloat(result.LONGITUDE),
},
});
}
}
}
catch (error) {
logger_js_1.logger.debug(`Failed to search for ${query}`, error);
}
}
}
catch (error) {
logger_js_1.logger.warn('Failed to get nearby amenities', error);
}
// Remove duplicates and sort by distance
const uniqueAmenities = amenities.filter((amenity, index, self) => index === self.findIndex(a => a.name === amenity.name && a.type === amenity.type));
return uniqueAmenities
.sort((a, b) => a.distance - b.distance)
.slice(0, 15); // Limit to 15 amenities
}
categorizeAmenity(query) {
const categoryMap = {
'shopping mall': 'shopping',
'hawker centre': 'food',
'food court': 'food',
'mrt station': 'transport',
'hospital': 'healthcare',
'school': 'education',
'community centre': 'recreation',
'park': 'recreation',
};
return categoryMap[query] || 'other';
}
inferAccessibilityInfo(busStop) {
// Basic accessibility inference based on description and location
const description = busStop.description.toLowerCase();
const roadName = busStop.roadName.toLowerCase();
return {
wheelchairAccessible: true, // Most Singapore bus stops are wheelchair accessible
sheltered: !description.includes('temp') && !description.includes('temporary'),
seatingAvailable: description.includes('int') || description.includes('interchange') ||
description.includes('terminal') || description.includes('hub'),
tactilePaving: true, // Most modern stops have tactile paving
audioAnnouncements: description.includes('mrt') || description.includes('interchange'),
notes: description.includes('temp') ? 'Temporary bus stop - facilities may be limited' : undefined,
};
}
async getLocationContext(busStop) {
const landmarks = [];
const transportHubs = [];
// Extract landmarks from description
const description = busStop.description;
const roadName = busStop.roadName;
// Common Singapore landmarks and transport hubs
const landmarkPatterns = [
/\b(mall|plaza|centre|center|hospital|school|park|garden|temple|mosque|church)\b/gi,
/\b(blk|block)\s*\d+[a-z]?/gi,
];
const transportPatterns = [
/\b(mrt|lrt|interchange|terminal|station)\b/gi,
];
for (const pattern of landmarkPatterns) {
const matches = description.match(pattern);
if (matches) {
landmarks.push(...matches.map(m => m.trim()));
}
}
for (const pattern of transportPatterns) {
const matches = description.match(pattern);
if (matches) {
transportHubs.push(...matches.map(m => m.trim()));
}
}
// Infer district from road name or description
let district;
const commonDistricts = [
'Jurong', 'Tampines', 'Woodlands', 'Sengkang', 'Punggol', 'Ang Mo Kio',
'Toa Payoh', 'Bishan', 'Orchard', 'Marina', 'Raffles', 'Chinatown',
'Little India', 'Bugis', 'Clarke Quay', 'Sentosa', 'Changi',
];
for (const dist of commonDistricts) {
if (description.toLowerCase().includes(dist.toLowerCase()) ||
roadName.toLowerCase().includes(dist.toLowerCase())) {
district = dist;
break;
}
}
return {
district,
area: roadName,
landmarks: [...new Set(landmarks)], // Remove duplicates
transportHubs: [...new Set(transportHubs)], // Remove duplicates
};
}
}
exports.BusStopDetailsTool = BusStopDetailsTool;