Skip to main content
Glama
anoopt

TfL Journey Status MCP Server

plan_journey

Plan journeys between two London locations using Transport for London's official journey planner. Input origin and destination to get route options.

Instructions

Plan journeys between two locations using the TfL Journey Planner.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
fromLocationYesOrigin location (station name, place name, or UK postcode).
toLocationYesDestination location (station name, place name, or UK postcode).
app_keyNoOptional application key for the API.

Implementation Reference

  • The core handler function that performs the TfL API call to plan journeys, handles disambiguation for ambiguous locations, processes responses, and extracts simplified journey summaries using helper functions.
    const executeFunction = async (args = {}) => { const { fromLocation, toLocation, app_key, ...queryParams } = args; if (!fromLocation) { throw new Error('fromLocation is required'); } if (!toLocation) { throw new Error('toLocation is required'); } const params = new URLSearchParams(); if (app_key) { params.append('app_key', app_key); } for (const [key, value] of Object.entries(queryParams)) { if (value === undefined || value === null || value === '') continue; params.append(key, value); } const query = params.toString(); const url = `https://api.tfl.gov.uk/journey/journeyresults/${encodeURIComponent( fromLocation )}/to/${encodeURIComponent(toLocation)}${query ? `?${query}` : ''}`; try { const response = await fetch(url, { method: 'GET' }); let data = null; try { data = await response.json(); } catch { data = null; } if (response.status === 300 && data) { const fromOptions = formatDisambiguation(data.fromLocationDisambiguation); const toOptions = formatDisambiguation(data.toLocationDisambiguation); let message = 'Multiple location matches found. Please specify which locations you meant:\n\n'; if (fromOptions.length > 0) { message += `**From "${fromLocation}" - choose one:**\n`; fromOptions.forEach((option, index) => { message += `${index + 1}. ${option.name} (${option.placeType || 'Station'})\n`; }); message += '\n'; } if (toOptions.length > 0) { message += `**To "${toLocation}" - choose one:**\n`; toOptions.forEach((option, index) => { message += `${index + 1}. ${option.name} (${option.placeType || 'Station'})\n`; }); message += '\n'; } message += '**Alternative:** You can also use UK postcodes for more precise locations (e.g., "SW19 7NE" for Wimbledon area).\n\n'; message += 'Please tell me which specific stations you want to use, or provide postcodes for your origin and destination.'; return { message, requiresUserChoice: true, fromOptions, toOptions, }; } if (!response.ok) { throw new Error( data ? JSON.stringify(data) : `Unexpected response with status ${response.status}` ); } // Extract simplified journey data for successful responses if (data?.journeys) { return { journeys: data.journeys.map(extractJourneySummary), searchCriteria: data.searchCriteria }; } return ( data ?? { message: 'Journey planned successfully, but no response body was returned.', } ); } catch (error) { console.error('Error fetching journey plan:', error); return { error: `An error occurred while fetching the journey plan: ${ error instanceof Error ? error.message : JSON.stringify(error) }`, }; } };
  • The tool definition object including the schema for input parameters (fromLocation, toLocation required; app_key optional) and metadata for the 'plan_journey' tool.
    const apiTool = { function: executeFunction, definition: { type: 'function', function: { name: 'plan_journey', description: 'Plan journeys between two locations using the TfL Journey Planner.', parameters: { type: 'object', properties: { fromLocation: { type: 'string', description: 'Origin location (station name, place name, or UK postcode).', }, toLocation: { type: 'string', description: 'Destination location (station name, place name, or UK postcode).', }, app_key: { type: 'string', description: 'Optional application key for the API.', }, }, required: ['fromLocation', 'toLocation'], additionalProperties: true, }, }, }, };
  • lib/tools.js:7-30 (registration)
    Dynamic registration of tools by importing 'apiTool' from files listed in toolPaths (which includes 'tfl/journey-planner.js'), handling name deduplication.
    export async function discoverTools() { const tools = await Promise.all( toolPaths.map(async (file) => { const { apiTool } = await import(`../tools/${file}`); return { ...apiTool, path: file }; }) ); // deduplicate tool names const nameCounts = {}; return tools.map((tool) => { const name = tool.definition?.function?.name; if (!name) return tool; nameCounts[name] = (nameCounts[name] || 0) + 1; if (nameCounts[name] > 1) { tool.definition.function.name = `${name}_${nameCounts[name]}`; } return tool; }); }
  • tools/paths.js:1-5 (registration)
    Configuration listing the tool files to dynamically load, including the journey-planner.js for 'plan_journey'.
    export const toolPaths = [ 'tfl/status.js', 'tfl/status-detail.js', 'tfl/journey-planner.js' ];
  • Helper functions used by plan_journey handler: extractJourneySummary simplifies journey data, formatDisambiguation handles ambiguous location matches.
    /** * Extracts clean journey summary from TfL journey data * @param {Object} journey - Raw journey object from TfL API * @returns {Object} - Simplified journey summary */ export const extractJourneySummary = (journey) => { return { startDateTime: journey.startDateTime, arrivalDateTime: journey.arrivalDateTime, duration: journey.duration, legs: journey.legs?.map(extractLegSummary) || [], alternativeRoute: journey.alternativeRoute }; }; /** * Extracts clean leg summary from TfL leg data * @param {Object} leg - Raw leg object from TfL API * @returns {Object} - Simplified leg summary */ export const extractLegSummary = (leg) => { const summary = { mode: leg.mode?.name || 'unknown', duration: leg.duration, departureTime: leg.departureTime, arrivalTime: leg.arrivalTime, from: leg.departurePoint?.commonName || 'Unknown', to: leg.arrivalPoint?.commonName || 'Unknown', instruction: leg.instruction?.summary || '', isDisrupted: leg.isDisrupted || false }; // Add distance for walking legs if (leg.mode?.name === 'walking' && leg.distance) { summary.distance = Math.round(leg.distance); summary.distanceUnit = 'metres'; } // Add route info for transit legs if (leg.routeOptions?.[0]) { const route = leg.routeOptions[0]; if (route.name) summary.routeName = route.name; if (route.direction) summary.direction = route.direction; } // Add platform info if available if (leg.departurePoint?.platformName) { summary.departurePlatform = leg.departurePoint.platformName; } if (leg.arrivalPoint?.platformName) { summary.arrivalPlatform = leg.arrivalPoint.platformName; } return summary; }; /** * Formats disambiguation data from TfL API response * @param {Object} disambiguation - Disambiguation object from TfL API * @returns {Array} - Array of top 3 normalized location options */ export const formatDisambiguation = (disambiguation) => { if (!disambiguation) return []; const options = disambiguation.matches || disambiguation.disambiguationOptions || []; const normalized = options.map((option) => { if (option.name) { const { id, name, lat, lon, matchQuality, modes, routeType, placeType, parameterStatus, } = option; return { parameterValue: id, name, matchQuality, modes, routeType, placeType, parameterStatus, lat, lon, }; } const { parameterValue, uri, place = {}, matchQuality, } = option; const { commonName, placeType, lat, lon, modes, naptanId, icsCode, } = place; return { parameterValue, uri, name: commonName, matchQuality, modes, placeType, lat, lon, naptanId, icsCode, }; }); return normalized .sort((a, b) => (b.matchQuality ?? 0) - (a.matchQuality ?? 0)) .slice(0, 3); };

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/anoopt/london-tfl-journey-status-mcp-server'

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