Skip to main content
Glama

TMB Bus Arrival Times

by esteveavi
server.tsβ€’7.28 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; import { TMBApiResponse, BusStopInfo, FormattedBusArrival, BusLineInfo, } from './types.js'; /** * TMB Bus Arrival MCP Server * * This server provides real-time bus arrival information from TMB's iBus service. * It exposes tools to query bus stops and get arrival times in minutes. */ class TMBBusServer { private server: Server; private appId: string; private appKey: string; private baseUrl = 'https://api.tmb.cat/v1/itransit/bus/parades'; constructor() { // Get credentials from environment variables this.appId = process.env.TMB_APP_ID || ''; this.appKey = process.env.TMB_APP_KEY || ''; if (!this.appId || !this.appKey) { console.error('ERROR: TMB_APP_ID and TMB_APP_KEY environment variables must be set'); process.exit(1); } // Initialize MCP server this.server = new Server( { name: 'tmb-bus-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } /** * Setup MCP tool handlers */ private setupToolHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.getTools(), })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => this.handleToolCall(request) ); } /** * Define available tools */ private getTools(): Tool[] { return [ { name: 'get_bus_arrivals', description: 'Get real-time bus arrival times for a specific TMB bus stop. ' + 'Returns the next arrivals for each bus line that serves the stop, ' + 'including time in minutes, destination, and bus line information.', inputSchema: { type: 'object', properties: { stopCode: { type: 'string', description: 'The TMB bus stop code (e.g., "2775", "108")', }, }, required: ['stopCode'], }, }, ]; } /** * Handle tool execution */ private async handleToolCall(request: any) { const { name, arguments: args } = request.params; try { if (name === 'get_bus_arrivals') { const stopCode = args.stopCode; const busInfo = await this.getBusArrivals(stopCode); return { content: [ { type: 'text', text: this.formatBusInfo(busInfo), }, ], }; } return { content: [ { type: 'text', text: `Unknown tool: ${name}`, }, ], isError: true, }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } /** * Fetch bus arrivals from TMB API */ private async getBusArrivals(stopCode: string): Promise<BusStopInfo> { const url = `${this.baseUrl}/${stopCode}`; const params = { app_id: this.appId, app_key: this.appKey, }; try { const response = await axios.get<TMBApiResponse>(url, { params }); return this.transformApiResponse(response.data); } catch (error) { if (axios.isAxiosError(error)) { throw new Error( `TMB API error: ${error.response?.status} - ${ error.response?.data?.message || error.message }` ); } throw error; } } /** * Transform TMB API response to a more friendly format */ private transformApiResponse(data: TMBApiResponse): BusStopInfo { const queryTime = new Date(data.timestamp); if (!data.parades || data.parades.length === 0) { throw new Error('Bus stop not found or no data available'); } const stop = data.parades[0]; const lines: BusLineInfo[] = []; for (const route of stop.linies_trajectes) { const arrivals = route.propers_busos.map((bus) => { const arrivalTime = new Date(bus.temps_arribada); const minutesUntilArrival = Math.round( (bus.temps_arribada - data.timestamp) / 1000 / 60 ); return { minutesUntilArrival: Math.max(0, minutesUntilArrival), arrivalTime: arrivalTime.toLocaleTimeString('es-ES'), busId: bus.id_bus, }; }); lines.push({ line: String(route.codi_linia ?? ''), lineName: route.nom_linia, destination: route.desti_trajecte, direction: route.id_sentit === 1 ? 'outbound' : 'return', arrivals: arrivals.sort((a, b) => a.minutesUntilArrival - b.minutesUntilArrival), }); } return { stopCode: stop.codi_parada, stopName: stop.nom_parada, queryTimestamp: queryTime.toLocaleString('es-ES'), lines: lines.sort((a, b) => a.line.localeCompare(b.line)), }; } /** * Format bus info for display */ private formatBusInfo(info: BusStopInfo): string { let output = `🚌 Bus Stop: ${info.stopName} (${info.stopCode})\n`; output += `πŸ“… Query Time: ${info.queryTimestamp}\n\n`; if (info.lines.length === 0) { output += 'No buses currently scheduled for this stop.\n'; return output; } for (const line of info.lines) { output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `🚍 Line ${line.lineName} (${line.line}) β†’ ${line.destination}\n`; output += ` Direction: ${line.direction}\n\n`; if (line.arrivals.length === 0) { output += ` No arrivals scheduled\n`; } else { output += ` Next arrivals:\n`; for (const arrival of line.arrivals.slice(0, 3)) { const timeStr = arrival.minutesUntilArrival === 0 ? 'πŸ”΄ Arriving now!' : `⏱️ ${arrival.minutesUntilArrival} min`; output += ` β€’ ${timeStr} (at ${arrival.arrivalTime}) - Bus #${arrival.busId}\n`; } } output += `\n`; } return output; } /** * Start the MCP server */ async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('TMB Bus MCP Server running on stdio'); } } // Start the server const server = new TMBBusServer(); server.run().catch(console.error);

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/esteveavi/ibus-mcp'

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