Transport NSW API Client MCP
by danhussey
Verified
from __future__ import print_function
from dotenv import load_dotenv
import os
import requests
from datetime import datetime
# Load environment variables
load_dotenv()
API_KEY = os.getenv('OPEN_TRANSPORT_API_KEY')
# Define common parameters for API requests
output_format = 'rapidJSON' # Required for JSON output
coord_output_format = 'EPSG:4326' # Standard coordinate format
incl_filter = 1 # Enable advanced filter mode
api_version = '10.2.1.42' # API version
# Import MCP server
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("Transport NSW")
@mcp.tool()
def find_transport_stops(location_coord, stop_type='BUS_POINT', radius=100):
"""
Find transport stops around a specific location.
Args:
location_coord (str): Coordinates in format 'LONGITUDE:LATITUDE:EPSG:4326'
stop_type (str): Type of stops to find: 'BUS_POINT', 'POI_POINT', or 'GIS_POINT'
radius (int): Search radius in meters
Returns:
API response with transport stops
"""
import requests
# API endpoint
API_ENDPOINT = 'https://api.transport.nsw.gov.au/v1/tp/coord'
# Set up the request parameters
params = {
'outputFormat': output_format,
'coord': location_coord,
'coordOutputFormat': coord_output_format,
'inclFilter': incl_filter,
'type_1': stop_type,
'radius_1': radius,
'version': api_version
}
# Set up the headers with the API key
headers = {
'Authorization': f'apikey {API_KEY}'
}
try:
# Make the request
response = requests.get(API_ENDPOINT, params=params, headers=headers)
# Check if the request was successful
if response.status_code == 200:
return response.json()
else:
print(f"Request failed with status code: {response.status_code}")
return None
except Exception as e:
print(f"Exception when calling Transport NSW API: {e}\n")
return None
@mcp.tool()
def get_transport_alerts(date=None, mot_type=None, stop_id=None, line_number=None, operator_id=None):
"""
Get transport alerts from the Transport NSW API.
Args:
date (str, optional): Date in DD-MM-YYYY format. Defaults to today's date.
mot_type (int, optional): Mode of transport type filter. Options:
1: Train
2: Metro
4: Light Rail
5: Bus
7: Coach
9: Ferry
11: School Bus
stop_id (str, optional): Stop ID or global stop ID to filter by.
line_number (str, optional): Line number to filter by (e.g., '020T1').
operator_id (str, optional): Operator ID to filter by.
Returns:
dict: API response containing alerts information
"""
import requests
# API endpoint - the alerts are under /v1/tp/add_info (with an underscore)
API_ENDPOINT = 'https://api.transport.nsw.gov.au/v1/tp/add_info'
# Set default date to today if not provided
if date is None:
date = datetime.now().strftime('%d-%m-%Y')
# Set up the request parameters
params = {
'outputFormat': output_format,
'filterDateValid': date,
'version': api_version
}
# Add optional filters if provided
if mot_type is not None:
params['filterMotType'] = mot_type
if stop_id is not None:
params['itdLPxxSelStop'] = stop_id
if line_number is not None:
params['itdLPxxSelLine'] = line_number
if operator_id is not None:
params['itdLPxxSelOperator'] = operator_id
# Ensure parameter names match what the API expects
# For the direct HTTP approach, some parameter names may be different than in Swagger
# Set up the headers with the API key
headers = {
'Authorization': f'apikey {API_KEY}'
}
try:
# Make the request
response = requests.get(API_ENDPOINT, params=params, headers=headers)
# Check if the request was successful
if response.status_code == 200:
return response.json()
else:
print(f"Request failed with status code: {response.status_code}")
return None
except Exception as e:
print(f"Exception when calling Transport NSW API: {e}\n")
return None
# Call the API to get real-time departure information for a specific stop
@mcp.tool()
def get_departure_monitor(stop_id, date=None, time=None, mot_type=None, max_results=1):
"""
Get real-time departure monitor information for a specific stop from the Trip Planner API.
This function uses direct HTTP requests to the Transport NSW API.
Args:
stop_id (str): Stop ID or global stop ID
date (str, optional): Date in DD-MM-YYYY format. Defaults to today's date.
time (str, optional): Time in HH:MM format. Defaults to current time.
mot_type (int, optional): Mode of transport type filter. Options:
1: Train
2: Metro
4: Light Rail
5: Bus
7: Coach
9: Ferry
11: School Bus
max_results (int, optional): Maximum number of results to return. Default is 1.
Returns:
list: Simplified list of departure information
"""
import requests
from datetime import datetime, timezone, timedelta
# API endpoint
API_ENDPOINT = 'https://api.transport.nsw.gov.au/v1/tp/departure_mon'
# Set default date and time to now if not provided
now = datetime.now()
# Format date as YYYYMMDD for the API
if date is None:
itd_date = now.strftime('%Y%m%d')
else:
# Convert from DD-MM-YYYY to YYYYMMDD
day, month, year = date.split('-')
itd_date = f"{year}{month}{day}"
# Format time as HHMM for the API
if time is None:
itd_time = now.strftime('%H%M')
else:
# Convert from HH:MM to HHMM
itd_time = time.replace(':', '')
# Parse the target time for later filtering
target_time = None
if time is not None:
time_parts = time.split(':')
hour = int(time_parts[0])
minute = int(time_parts[1]) if len(time_parts) > 1 else 0
# Create a datetime object with today's date and the specified time
# Make it timezone-aware to match the converted API times
target_time = datetime.now().replace(hour=hour, minute=minute, second=0, microsecond=0)
# Add timezone info to make it comparable with timezone-aware datetimes
target_time = target_time.astimezone()
# Always request more results than needed to ensure we have enough for filtering
# Set up the request parameters exactly as in the documentation
params = {
'outputFormat': 'rapidJSON',
'coordOutputFormat': 'EPSG:4326',
'mode': 'direct',
'type_dm': 'stop',
'name_dm': stop_id,
'depArrMacro': 'dep',
'itdDate': itd_date,
'itdTime': itd_time,
'TfNSWDM': 'true',
'version': api_version,
'radius_dm': 100,
'limit': max(20, max_results * 2) # Request more results than needed for better filtering
}
# Add mot_type filter if provided
if mot_type is not None:
params['motType'] = mot_type
# Set up the headers with the API key
headers = {
'Authorization': f'apikey {API_KEY}'
}
try:
# Make the request
response = requests.get(API_ENDPOINT, params=params, headers=headers)
# Check if the request was successful
if response.status_code == 200:
# Parse the JSON response
data = response.json()
# Limit the number of stop events to max_results if specified
if 'stopEvents' in data and len(data['stopEvents']) > max_results:
data['stopEvents'] = data['stopEvents'][:max_results]
print(f"Limited results to {max_results} departures")
# Process response
stops = data.get('stopEvents', [])
# Process stops and prepare for filtering/sorting
processed_stops = []
# Get current date and time for filtering
now_local = datetime.now()
today_date = now_local.strftime('%Y-%m-%d')
# Convert all departure times to datetime objects for easier processing
for stop in stops:
departure_time = stop.get('departureTimePlanned', '')
if not departure_time: # Skip entries without departure time
continue
# Parse the departure time (API returns times in UTC with Z suffix)
try:
# Remove any fractional seconds and handle Z suffix
clean_time = departure_time.split('.')[0]
if clean_time.endswith('Z'):
clean_time = clean_time[:-1] # Remove Z suffix
# Parse the UTC time from the API
departure_dt_utc = datetime.strptime(clean_time, "%Y-%m-%dT%H:%M:%S")
departure_dt_utc = departure_dt_utc.replace(tzinfo=timezone.utc)
# Convert to local time for easier comparison
departure_dt_local = departure_dt_utc.astimezone()
# Add the parsed datetime to the stop for easier sorting
stop['departure_dt_local'] = departure_dt_local
# Only include departures from today or future dates
departure_date = departure_dt_local.strftime('%Y-%m-%d')
if departure_date >= today_date:
processed_stops.append(stop)
except ValueError:
print(f"Could not parse departure time: {departure_time}")
continue
# Sort the stops based on time parameter if provided
if target_time is not None:
# Calculate time difference for each stop
for stop in processed_stops:
departure_dt_local = stop['departure_dt_local']
# Calculate time difference in seconds
time_diff = abs((departure_dt_local - target_time).total_seconds())
stop['time_diff'] = time_diff
# Sort by time difference (closest to target time first)
processed_stops.sort(key=lambda x: x.get('time_diff', float('inf')))
else:
# Sort by departure time if no specific time is provided (earliest first)
processed_stops.sort(key=lambda x: x['departure_dt_local'])
# Limit to max_results
if max_results > 0 and len(processed_stops) > max_results:
processed_stops = processed_stops[:max_results]
print(f"Limited results to {max_results} departures")
# Create a more concise version of the data for LLMs
concise_stops = []
for stop in processed_stops:
# Format local time for better readability
local_time = stop['departure_dt_local'].strftime('%Y-%m-%d %H:%M:%S')
# Extract only the essential information
concise_stop = {
'stop_name': stop.get('location', {}).get('name', ''),
'route_number': stop.get('transportation', {}).get('number', ''),
'route_name': stop.get('transportation', {}).get('description', ''),
'destination': stop.get('transportation', {}).get('destination', {}).get('name', ''),
'operator': stop.get('transportation', {}).get('operator', {}).get('name', ''),
'planned_departure': stop.get('departureTimePlanned', ''),
'estimated_departure': stop.get('departureTimeEstimated', ''),
'local_departure_time': local_time,
'wheelchair_access': stop.get('properties', {}).get('WheelchairAccess', 'false')
}
concise_stops.append(concise_stop)
return concise_stops
else:
print(f"Request failed with status code: {response.status_code}")
print(f"Response text: {response.text[:500]}...") # Print first 500 chars
return None
except Exception as e:
print(f"Exception when calling Transport NSW API: {e}\n")
return None