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);