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);
    };
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure but only states the basic function without details on rate limits, authentication needs, error handling, or what the response includes. It mentions using the TfL Journey Planner but doesn't explain behavioral traits like real-time data usage or potential costs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose without any wasted words. It directly communicates the tool's function in a clear and structured manner, making it easy to understand at a glance.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (journey planning with 3 parameters) and no output schema, the description is minimally complete but lacks details on return values, error cases, or integration context. It covers the basic purpose but doesn't fully compensate for the absence of annotations or output schema, leaving gaps in understanding how to use it effectively.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 100%, so the input schema already documents all parameters thoroughly. The description adds no additional meaning beyond what the schema provides, such as examples or constraints, but doesn't contradict it, meeting the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Plan journeys') and resource ('between two locations using the TfL Journey Planner'), with a precise verb that distinguishes it from sibling tools like get_line_status and get_line_status_detail, which focus on line status rather than journey planning.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives or any context for its application. It lacks information about prerequisites, such as needing an app_key for certain API calls, or when not to use it, leaving usage entirely implicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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