Skip to main content
Glama

🚂 Caltrain MCP Server (Because You Love Waiting for Trains)

PyPI CI & Semantic release

Caltrain MCP Demo

A Model Context Protocol (MCP) server that promises to tell you exactly when the next Caltrain will arrive... and then be 10 minutes late anyway. Uses real GTFS data, so at least the disappointment is official!

Features (Or: "Why We Built This Thing")

  • 🚆 "Real-time" train schedules - Get the next departures between any two stations (actual arrival times may vary by +/- infinity)

  • 📍 Station lookup - Because apparently 31 stations is too many to memorize 🤷‍♀️

  • 🕐 Time-specific queries - Plan your commute with surgical precision, then watch it all fall apart

  • Smart search - Type 'sf' instead of the full name because we're all lazy here

  • 📊 GTFS-based - We use the same data Caltrain does, so when things go wrong, we can blame them together

Related MCP server: Google Calendar MCP Server

Setup (The Fun Part 🙄)

  1. Install dependencies (aka "More stuff to break"):

    # Install uv if you haven't already (because pip is apparently too mainstream now)
    curl -LsSf https://astral.sh/uv/install.sh | sh
    
    # Install dependencies using uv (fingers crossed it actually works)
    uv sync
  2. Get that sweet, sweet GTFS data: The server expects Caltrain GTFS data in the src/caltrain_mcp/data/caltrain-ca-us/ directory. Because apparently we can't just ask the trains nicely where they are.

    uv run python scripts/fetch_gtfs.py

    This magical script downloads files that contain:

    • stops.txt - All the places trains pretend to stop

    • trips.txt - Theoretical journeys through space and time

    • stop_times.txt - When trains are supposed to arrive (spoiler: they don't)

    • calendar.txt - Weekday vs weekend schedules (because trains also need work-life balance)

Usage (Good Luck!)

As an MCP Server (The Real Deal)

This server is designed to be used with MCP clients like Claude Desktop, not run directly by humans (because that would be too easy). Here's how to actually use it:

With Claude Desktop

Add this to your Claude Desktop MCP configuration file:

{
  "mcpServers": {
    "caltrain": {
      "command": "uvx",
      "args": ["caltrain-mcp"]
    }
  }
}

This will automatically install and run the latest version from PyPI.

Then restart Claude Desktop and you'll have access to Caltrain schedules directly in your conversations!

With Other MCP Clients

Any MCP-compatible client can use this server by starting it with:

uvx caltrain-mcp

The server communicates via stdin/stdout using the MCP protocol. It doesn't do anything exciting when run directly - it just sits there waiting for proper MCP messages.

Testing the Server (For Development)

You can test if this thing actually works by importing it directly:

from caltrain_mcp.server import next_trains, list_stations

# Test next trains functionality (prepare for disappointment)
result = await next_trains('San Jose Diridon', 'San Francisco')
print(result)  # Spoiler: there are no trains

# Test stations list (all 31 of them, because apparently that's manageable)
stations = await list_stations()
print(stations)

Available Tools (Your New Best Friends)

next_trains(origin, destination, when_iso=None)

Ask politely when the next train will show up. The server will consult its crystal ball (GTFS data) and give you times that are technically accurate.

Parameters:

  • origin (str): Where you are now (probably regretting your life choices)

  • destination (str): Where you want to be (probably anywhere but here)

  • when_iso (str, optional): When you want to travel (as if time has any meaning in public transit)

Examples:

# Next trains from current time (aka "right now would be nice")
next_trains('San Jose Diridon', 'San Francisco')

# Trains at a specific time (for the optimists who think schedules matter)
next_trains('Palo Alto', 'sf', '2025-05-23T06:00:00')

# Using abbreviations (because typing is hard)
next_trains('diridon', 'sf')

list_stations()

Get a list of all 31 Caltrain stations, because memorizing them is apparently too much to ask.

Returns: A formatted list that will make you realize just how many places this train supposedly goes.

Station Name Recognition (We're Not Mind Readers, But We Try)

The server supports various ways to be lazy about typing station names:

  • Full names: "San Jose Diridon Station" (for the perfectionists)

  • Short names: "San Francisco" (for the slightly less perfectionist)

  • Abbreviations: "sf" → "San Francisco" (for the truly lazy)

  • Partial matching: "diridon" matches "San Jose Diridon Station" (for when you can't be bothered)

Available Stations (All 31 Glorious Stops)

The server covers every single Caltrain station because we're completionists:

San Francisco to San Jose (The Main Event):

  • San Francisco, 22nd Street, Bayshore, South San Francisco, San Bruno, Millbrae, Broadway, Burlingame, San Mateo, Hayward Park, Hillsdale, Belmont, San Carlos, Redwood City, Menlo Park, Palo Alto, Stanford, California Avenue, San Antonio, Mountain View, Sunnyvale, Lawrence, Santa Clara, College Park, San Jose Diridon

San Jose to Gilroy (The "Why Does This Exist?" Extension):

  • Tamien, Capitol, Blossom Hill, Morgan Hill, San Martin, Gilroy

Sample Output (Prepare to Be Amazed)

🚆 Next Caltrain departures from San Jose Diridon Station to San Francisco Caltrain Station on Thursday, May 22, 2025:
• Train 153: 17:58:00 → 19:16:00 (to San Francisco)
• Train 527: 18:22:00 → 19:22:00 (to San Francisco)
• Train 155: 18:28:00 → 19:46:00 (to San Francisco)
• Train 429: 18:43:00 → 19:53:00 (to San Francisco)
• Train 157: 18:58:00 → 20:16:00 (to San Francisco)

Actual arrival times may vary. Side effects may include existential dread and a deep appreciation for remote work.

Technical Details (For the Nerds)

  • GTFS Processing: We automatically handle the relationship between stations and their platforms (because apparently trains are complicated)

  • Service Calendar: Respects weekday/weekend schedules (trains also need their beauty rest)

  • Data Types: Handles the chaos that is mixed integer/string formats in GTFS files

  • Time Parsing: Supports 24+ hour format for those mythical late-night services

  • Error Handling: Gracefully fails when you type "Narnia" as a station name

Project Structure (The Organized Chaos)

caltrain-mcp/
├── .github/workflows/         # GitHub Actions (the CI/CD overlords)
│   ├── ci.yml                 # Main CI pipeline (linting, testing, the works)
│   └── update-gtfs.yml        # Automated GTFS data updates
├── src/caltrain_mcp/          # Main package (because modern Python demands structure)
│   ├── data/caltrain-ca-us/   # GTFS data storage (where CSV files go to retire)
│   ├── __init__.py            # Package initialization (the ceremony of Python)
│   ├── __main__.py            # Entry point for python -m caltrain_mcp
│   ├── server.py              # MCP server implementation (where the magic happens)
│   └── gtfs.py                # GTFS data processing (aka "CSV wrestling")
├── scripts/                   # Utility scripts (the supporting cast)
│   ├── __init__.py            # Makes scripts a proper Python package
│   ├── fetch_gtfs.py          # Downloads the latest disappointment data
│   └── lint.py                # Run all CI checks locally (before embarrassment)
├── tests/                     # Test suite (because trust but verify)
│   ├── conftest.py            # Shared test fixtures (the common ground)
│   ├── test_gtfs.py           # GTFS functionality tests (8 tests of data wrangling)
│   ├── test_server.py         # Server functionality tests (4 tests of MCP protocol)
│   └── test_fetch_gtfs.py     # Data fetching tests (7 tests of download chaos)
├── .pre-commit-config.yaml    # Pre-commit hooks configuration
├── pyproject.toml             # Modern Python config (because setup.py is so 2020)
└── README.md                  # This literary masterpiece

Development & Testing (For When Things Inevitably Break)

Code Quality & CI/CD

This project uses modern Python tooling to keep the code clean and maintainable:

  • Ruff: Lightning-fast linting and formatting (because life's too short for slow tools)

  • MyPy: Type checking (because guessing types is for amateurs)

  • Pytest: Testing framework with coverage reporting

Release Process (Automated Awesomeness)

This project uses automated versioning and publishing:

  • Semantic Versioning: Version numbers are automatically determined from commit messages using Conventional Commits

  • Automatic Tagging: When you push to main, semantic-release creates version tags automatically

  • PyPI Publishing: Tagged releases are automatically built and published to PyPI via GitHub Actions

  • Trusted Publishing: Uses OIDC authentication with PyPI (no API tokens needed!)

Making a Release

Just commit using conventional commit format and push to main:

# For bug fixes (patch version bump: 1.0.0 → 1.0.1)
git commit -m "fix: correct station name lookup bug"

# For new features (minor version bump: 1.0.0 → 1.1.0)
git commit -m "feat: add support for weekend schedules"

# For breaking changes (major version bump: 1.0.0 → 2.0.0)
git commit -m "feat!: redesign API structure"
# or
git commit -m "feat: major API changes

BREAKING CHANGE: This changes the function signatures"

The semantic-release workflow will:

  1. Analyze your commit messages

  2. Determine the appropriate version bump

  3. Create a git tag (e.g., v1.2.3)

  4. Generate a changelog

  5. Trigger the release workflow to publish to PyPI

Local Testing

Test the build process locally before pushing:

# Build packages locally
uv run python -m build --sdist --wheel

# Validate packages
uv run twine check dist/*

# Test upload to Test PyPI (optional)
uv run twine upload --repository testpypi dist/*

GitHub Actions CI

Every PR and push to main triggers automatic checks:

  • Linting: Ruff checks for code quality issues

  • Formatting: Ensures consistent code style

  • Type Checking: MyPy validates type annotations

  • Tests: Full test suite with coverage reporting

  • Coverage: Test coverage reporting in CI logs

The CI will politely reject your PR if any checks fail, because standards matter.

MCP Integration (For the AI Overlords)

This server implements the Model Context Protocol (MCP), which means it's designed to work seamlessly with AI assistants and other MCP clients. Once configured:

  • Claude Desktop: Ask Claude about train schedules directly in conversation

  • Other MCP Clients: Any MCP-compatible tool can access Caltrain data

  • Real-time Integration: Your AI can check schedules, suggest routes, and help plan trips

  • Natural Language: No need to remember station names or command syntax

The server exposes two main tools:

  • next_trains - Get upcoming departures between stations

  • list_stations - Browse all available Caltrain stations

So your AI assistant can now disappoint you about train schedules just like a real human would! The future is truly here.

This project uses official Caltrain GTFS data. If something goes wrong, blame them, not us. We're just the messenger.


Built with ❤️ and a concerning amount of caffeine in the Bay Area, where public transit is both a necessity and a source of eternal suffering.

Available Tools

2 tools
list_stationsA

List all available Caltrain stations.

This tool is useful when you need to find the exact station names, especially if the next_trains() tool returns a "Station not found" error. Station names are case-insensitive and support some common abbreviations like 'SF' and 'SJ'.

Returns a formatted list of all Caltrain stations that can be used as origin or destination in the next_trains() tool.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.6/5.0
Behavior4/5

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

No annotations are provided, so the description carries the full transparency burden. It reveals case-insensitive matching and common abbreviations, but does not specify the response format beyond 'formatted list'.

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 three sentences, each earning its place: first sentence states purpose, second adds usage context, third details behavior. No unnecessary words.

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

Completeness4/5

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

For a parameterless tool with no output schema, the description adequately covers input and output behavior. It explains the output is a 'formatted list', which is acceptable, though additional output structure details would improve completeness.

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

Parameters4/5

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

The tool has no parameters, so the description trivially meets requirements. Baseline score of 4 applies; no additional param details needed.

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 precisely states the tool lists all Caltrain stations, explains its utility for resolving errors from next_trains, and distinguishes itself from the sibling tool by focusing on station name discovery.

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

Usage Guidelines5/5

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

Explicitly provides a concrete use case: when next_trains returns 'Station not found', making the when-to-use clear and actionable.

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

next_trainsA

Return the next few scheduled Caltrain departures.

Args: origin: Station name (e.g. 'San Jose Diridon', 'Palo Alto', 'San Francisco'). Supports common abbreviations like 'SF' for San Francisco, 'SJ' for San Jose. If station is not found, use list_stations() to see all available options. destination: Station name (e.g. 'San Francisco', 'Mountain View', 'Tamien'). Supports common abbreviations like 'SF' for San Francisco, 'SJ' for San Jose. If station is not found, use list_stations() to see all available options. when_iso: Optional ISO-8601 datetime (local time). Default: now.

Note: If you get a "Station not found" error, try using the list_stations() tool first to see exact station names, then retry with the correct spelling.

ParametersJSON Schema
NameRequiredDescriptionDefault
originYes
when_isoNo
destinationYes

TDQS

A4.6/5.0
Behavior4/5

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

With no annotations, description carries full burden. It discloses error handling ('Station not found' leads to list_stations) and notes default time behavior. Could add more on return format or rate limits, but covers key traits.

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

Conciseness4/5

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

Description is front-loaded with purpose, then breaks into args with docstring style, and ends with a helpful note. Slightly verbose but efficient for the complexity. Could merge the last sentence into args section.

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

Completeness4/5

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

Given no output schema and 3 parameters, description covers key aspects: purpose, parameters, error handling, and sibling reference. Lacks explicit mention of output structure (e.g., list of departure times), but the phrase 'next few scheduled Caltrain departures' implies a list, which is sufficient.

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

Parameters5/5

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

Schema coverage is 0%, so description must compensate. It fully explains 'origin' and 'destination' with station name examples and abbreviations, and 'when_iso' with ISO-8601 format and default. Adds practical tips for misspellings.

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 tool returns 'the next few scheduled Caltrain departures', with specific verb and resource. It distinguishes from sibling 'list_stations' by focusing on departures between two stations.

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

Usage Guidelines5/5

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

Explicitly explains when to use this tool (for departures), provides alternative when station not found ('use list_stations()'), and gives examples of acceptable inputs. No ambiguity about usage context.

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

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv1.0.0
    • First observedlist_stations
    • First observednext_trains

TDQS

A4.4/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: one lists all stations, the other retrieves upcoming departures. There is no overlap in functionality.

Naming Consistency4/5

Tool names are readable and follow a descriptive pattern, but 'list_stations' uses verb_noun while 'next_trains' uses adjective_noun, which is a minor inconsistency.

Tool Count3/5

With only 2 tools, it covers the basic station listing and departure lookup, but feels minimal for a transportation server. Additional tools like route or alert info would be expected for completeness.

Completeness4/5

The tool set covers the core workflow of finding stations and getting departures. However, there is no way to get details about a specific train or receive alerts, which are minor gaps.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers