/**
* Malaysia Transit Tools
* Tools for accessing real-time bus and train information
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import axios from 'axios';
import { detectAreaFromLocation, getAreaStateMapping } from './geocoding.utils.js';
// Get middleware URL from environment
const getMiddlewareUrl = (): string => {
return process.env.MIDDLEWARE_URL || 'http://localhost:3000';
};
/**
* Register all transit-related tools
*/
export function registerTransitTools(server: McpServer): void {
// ============================================================================
// SERVICE AREA TOOLS
// ============================================================================
server.tool(
'list_service_areas',
'List all available transit service areas in Malaysia (e.g., Klang Valley, Penang, Kuantan)',
{},
async () => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/areas`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Failed to fetch service areas',
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
server.tool(
'detect_location_area',
'Automatically detect which transit service area a location belongs to using geocoding. Use this when the user mentions a place name without specifying the area (e.g., "KTM Alor Setar", "Komtar", "KLCC")',
{
location: z.coerce.string().describe('Location name or place (e.g., "KTM Alor Setar", "Komtar", "Pavilion KL")'),
},
async ({ location }) => {
try {
const result = await detectAreaFromLocation(location);
if (result) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
area: result.area,
confidence: result.confidence,
location: result.location,
source: result.source,
message: `Location "${location}" detected in service area: ${result.area}`,
}, null, 2),
},
],
};
} else {
// Return available areas if detection fails
const areaMapping = getAreaStateMapping();
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `Could not automatically detect service area for "${location}". Please specify the area manually.`,
availableAreas: areaMapping,
}, null, 2),
},
],
};
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Failed to detect location area',
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
server.tool(
'get_area_info',
'Get detailed information about a specific transit service area',
{
areaId: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley", "kuantan")'),
},
async ({ areaId }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/areas/${areaId}`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch area info for ${areaId}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
// ============================================================================
// STOP TOOLS
// ============================================================================
server.tool(
'search_stops',
'Search for bus or train stops by name in a specific area. IMPORTANT: If you are unsure which area a location belongs to, use detect_location_area first to automatically determine the correct area.',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley"). Use detect_location_area if unsure.'),
query: z.coerce.string().describe('Search query (e.g., "Komtar", "KLCC")'),
},
async ({ area, query }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/stops/search`, {
params: { area, q: query },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to search stops in ${area}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
server.tool(
'get_stop_details',
'Get detailed information about a specific bus or train stop',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley")'),
stopId: z.coerce.string().describe('Stop ID from search results'),
},
async ({ area, stopId }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/stops/${stopId}`, {
params: { area },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch stop details for ${stopId}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
server.tool(
'get_stop_arrivals',
'Get real-time arrival predictions for buses/trains at a specific stop',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley")'),
stopId: z.coerce.string().describe('Stop ID from search results'),
},
async ({ area, stopId }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/stops/${stopId}/arrivals`, {
params: { area },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch arrivals for stop ${stopId}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
server.tool(
'find_nearby_stops',
'Find bus or train stops near a specific location',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley")'),
lat: z.coerce.number().describe('Latitude coordinate'),
lon: z.coerce.number().describe('Longitude coordinate'),
radius: z.coerce.number().optional().default(500).describe('Search radius in meters (default: 500)'),
},
async ({ area, lat, lon, radius }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/stops/nearby`, {
params: { area, lat, lon, radius },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to find nearby stops`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
// ============================================================================
// ROUTE TOOLS
// ============================================================================
server.tool(
'list_routes',
'List all available bus or train routes in a specific area',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley")'),
},
async ({ area }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/routes`, {
params: { area },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch routes for ${area}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
server.tool(
'get_route_details',
'Get detailed information about a specific route including stops and geometry',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley")'),
routeId: z.coerce.string().describe('Route ID from list_routes'),
},
async ({ area, routeId }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/routes/${routeId}`, {
params: { area },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch route details for ${routeId}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
server.tool(
'get_route_geometry',
'Get the geographic path and stops for a specific route (for map visualization)',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley")'),
routeId: z.coerce.string().describe('Route ID from list_routes'),
},
async ({ area, routeId }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/routes/${routeId}/geometry`, {
params: { area },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch route geometry for ${routeId}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
// ============================================================================
// REAL-TIME DATA TOOLS
// ============================================================================
server.tool(
'get_live_vehicles',
'Get real-time positions of all buses and trains in a specific area',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley")'),
type: z.enum(['bus', 'rail']).optional().describe('Filter by transit type (optional)'),
},
async ({ area, type }) => {
try {
const params: any = { area };
if (type) {
params.type = type;
}
const response = await axios.get(`${getMiddlewareUrl()}/api/realtime`, {
params,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch live vehicles for ${area}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
server.tool(
'get_provider_status',
'Check the operational status of transit providers in a specific area',
{
area: z.coerce.string().describe('Service area ID (e.g., "penang", "klang-valley")'),
},
async ({ area }) => {
try {
const response = await axios.get(`${getMiddlewareUrl()}/api/areas/${area}/providers/status`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch provider status for ${area}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
);
}