STPT MCP
STPT MCP is an unofficial read-only MCP server for Timișoara public transport, exposing live and schedule transit data through seven tools over stdio or Streamable HTTP.
Search stops by ID, name, or line with diacritic-insensitive matching.
Get stop details: coordinates, serving lines, directions, terminals, and neighboring stops.
List tram, trolleybus, bus, metropolitan, express, and vaporetto routes.
Get upcoming arrivals with line/limit filters, live-first behavior, GTFS schedule fallback, and explicit freshness/provenance metadata.
Get current or historical service alerts filtered by stop, line, and active status.
Get vehicle positions with line filters, observation timestamps, and staleness flags.
Check server readiness, version, network feed metadata, live-feed freshness, and cache statistics.
Use bundled offline stop/route discovery, structured errors, and secure HTTP options including bearer auth and OAuth sign-in.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@STPT MCPWhen is the next tram at Piața Sfânta Maria?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
STPT MCP
An unofficial, standalone Model Context Protocol server for STPT public transport in Timișoara, Romania. It exposes live arrivals, date-aware schedule fallback, service alerts, vehicle positions, and an offline stop/route index to MCP clients over stdio or Streamable HTTP.
This project is not affiliated with, endorsed by, or operated by STPT or the Timișoara municipality. Upstream data is used with caching and conservative request limits. Do not use it for safety-critical decisions.
In action

Ask in plain language. The assistant calls search_stops to resolve the stop,
then get_arrivals for the departures, and answers with the next bus. The same
tools work from any MCP client — Claude, Hermes, Cursor, or an agent of your
own.
Related MCP server: @cyanheads/onebusaway-mcp-server
Highlights
Seven read-only MCP tools with typed output schemas.
Live-first arrivals with date- and shape-aware schedule fallback, explicit provenance, and last-known-good stale data.
Bounded TTL caches with single-flight refresh. Fifty concurrent calls to one stop produce one live upstream request, not fifty.
Bounded upstream concurrency, timeouts, retries with jitter,
Retry-Aftersupport, and response-size limits.Bundled GTFS-derived network snapshot, so stop/route discovery works offline.
stdio and Streamable HTTP through the official MCP Python SDK.
Secure HTTP defaults: loopback bind, DNS-rebinding protection, Host/Origin validation, body limits, bounded sessions/concurrency, optional bearer auth, and built-in OAuth 2.1 sign-in for hosted clients.
Deterministic test suite with real MCP client tests for stdio and HTTP.
Requirements
Python 3.11 or newer.
uv for development (optional for end users).
Network access to STPT endpoints for live data. Discovery tools work offline.
Install
From PyPI, with one command:
pip install stpt-mcp
stpt-mcp --versionOr run it without installing anything permanently, using uv:
uvx stpt-mcp --versionFrom a checkout:
uv sync --all-groups
uv run stpt-mcp --versionOr from a wheel/sdist:
python -m venv .venv
. .venv/bin/activate
pip install dist/stpt_mcp-0.3.7-py3-none-any.whl
stpt-mcp --versionstpt-mcp with no subcommand is equivalent to stpt-mcp serve and defaults to
stdio.
Quick start
stdio
uv run stpt-mcp serve --transport stdioThe simplest MCP client configuration lets uvx fetch the package from PyPI
on first use:
{
"mcpServers": {
"stpt": {
"command": "uvx",
"args": ["stpt-mcp", "serve", "--transport", "stdio"]
}
}
}A minimal MCP client configuration using an installed console script:
{
"mcpServers": {
"stpt": {
"command": "stpt-mcp",
"args": ["serve", "--transport", "stdio"]
}
}
}From a checkout without installing the script:
{
"mcpServers": {
"stpt": {
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/stpt-mcp",
"stpt-mcp",
"serve",
"--transport",
"stdio"
]
}
}
}The server writes logs to stderr and never writes protocol data to stdout.
Streamable HTTP
uv run stpt-mcp serve --transport http
# endpoint: http://127.0.0.1:8000/mcp
# health: http://127.0.0.1:8000/healthzGenerate a bearer token for any non-local deployment:
export STPT_MCP_AUTH_TOKEN="$(stpt-mcp token)"
stpt-mcp serve --transport httpClients then send Authorization: Bearer <token>. /healthz stays
unauthenticated.
For hosted clients that support OAuth, set a public origin and a sign-in password instead of handing out a header token:
export STPT_MCP_PUBLIC_URL=https://mcp.example.com
export STPT_MCP_OAUTH_PASSWORD='replace-with-a-long-random-password'
stpt-mcp serve --transport httpClaude's custom connector can then use Sign in now with Register automatically. See OAuth sign-in for the reverse-proxy paths and connector steps.
Minimal client configuration:
{
"mcpServers": {
"stpt": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}HTTP is bound to 127.0.0.1 by default. Binding elsewhere requires an explicit
STPT_MCP_ALLOWED_HOSTS allowlist. If you expose the server beyond localhost,
also set STPT_MCP_AUTH_TOKEN and terminate TLS at a reverse proxy. For a
complete public deployment, see Public HTTPS,
OAuth sign-in, Deployment, and
Security.
Tools
Tool | Purpose |
| Search stops by ID, name, or line (diacritic-insensitive). |
| Stop coordinates, lines, directions, destinations, neighbors. |
| Routes with mode, category, and colors. |
| Live arrivals, with schedule fallback and line/limit filters. |
| Service alerts with active-period and stop/line filters. |
| Vehicle positions with observation timestamps and staleness. |
| Readiness, version, feed metadata, cache statistics. |
Every result is a typed envelope. Data-source problems are returned as
status: "partial" or "error" with a structured error, not a traceback:
{
"status": "partial",
"error": {
"code": "upstream_http_error",
"message": "Live arrivals were unavailable; schedule data is being used.",
"retryable": true,
"upstream": "live arrivals"
},
"provenance": {
"source": "schedule",
"fetched_at": "2026-10-21T10:30:00Z",
"observed_at": null,
"is_stale": false,
"data_age_seconds": 0.0,
"fallback_reason": "live_unavailable",
"warnings": ["Live arrivals were unavailable; trying the schedule."]
},
"stop_id": "59",
"stop_name": "Piața Sfânta Maria",
"line_filter": null,
"lookahead_minutes": 180,
"count": 9,
"arrivals": []
}arrivals is abbreviated above. Full schemas are generated from the Pydantic
models and exposed through MCP. See Tools for arguments,
semantics, error codes, and more examples.
Configuration
Configuration is read from environment variables; a few common values also have CLI flags. Defaults are safe for local use.
Variable | Default | Purpose |
|
|
|
|
| HTTP bind address. |
|
| HTTP port. |
|
| Streamable HTTP path. |
|
| Python log level. |
| unset | Optional static bearer token (16+ chars). |
| loopback | Required for non-loopback binds. |
| loopback | Browser origins allowed by the SDK. |
|
| HTTP request body limit in bytes. |
|
| HTTP concurrency cap. |
TTLs, stale windows, upstream timeouts, retry policy, and cache bounds are also configurable. See Configuration for the complete table.
How data selection works
A fresh cached arrival snapshot for the stop is returned with no request.
Otherwise live predictions are fetched. They are used only if their freshness is verified against the real clock: the feed's implied clock must match it, must not stand still across fetches, and must move with real time.
If live is empty, unavailable, stale, or unverified, the departures come from the bundled GTFS timetable for the local service date (
source: "schedule",live: false). No website is contacted for this.If the refresh fails, last-known-good data may be served inside the stale window.
If no usable data remains, the tool returns a structured error. Stale or unverified live predictions are never returned as arrivals, because frozen minutes counted from now look like real departures.
provenance.source identifies live, schedule, or network data;
provenance.is_stale and data_age_seconds quantify freshness;
fallback_reason explains why live data was not used. Live arrivals expose no
upstream observation timestamp, so observed_at is the fetch time for live
arrivals and null for schedules.
Development
uv sync --all-groups
uv run ruff format --check .
uv run ruff check .
uv run mypy
uv run pytest
uv run pytest --cov --cov-report=term-missing
uv run bandit -c pyproject.toml -r src
uv run pip-audit
uv buildThe default suite never contacts live STPT services. It uses deterministic
fixtures, injected clocks, httpx.MockTransport, and loopback fake upstreams.
An opt-in live smoke test is available:
STPT_MCP_RUN_NETWORK_TESTS=1 uv run pytest -m networkSee Testing, Architecture, and Contributing.
Related projects
The same STPT data, exposed for different runtimes. If one of these fits your setup, the others probably do too.
Project | Runtime | Use it for |
STPT MCP (this repo) | Any MCP client | Talking to your transport data |
Home Assistant | Automations, sensors, and dashboards | |
Raspberry Pi | A glanceable e-ink departures display |
All three are unofficial and are not affiliated with STPT.
Limitations
Upstream endpoints are unofficial public interfaces and may change without notice.
The bundled network snapshot and timetable are point-in-time GTFS-derived artifacts that end on 2026-12-31; after that the schedule fallback reports
schedule_unavailableuntil a release regenerates them. The feed version and service range are exposed byget_service_status.STPT's live arrivals feed carries no timestamps. Freshness is inferred from its display times, so the first fetch of a stop after a restart cannot prove the feed is running.
Each process has its own in-memory cache; multi-worker deployments do not share entries.
OAuth is single-owner and keeps tokens in memory; a restart requires clients to sign in again. It is not a multi-user identity provider.
Live arrivals do not carry an upstream observation timestamp.
Author
Created and maintained by Vlad Ohai (GitHub).
License and attribution
MIT licensed, © 2026 Vlad Ohai. STPT data remains the property of its respective owners. See LICENSE and NOTICE. OpenStreetMap-derived coordinates, if present, are © OpenStreetMap contributors under the ODbL.
Available Tools
7 toolsget_alertsGet STPT service alertsARead-onlyIdempotent
Get current or historical STPT service alerts. Filters by stop ID and/or line when supplied. Active periods are evaluated in Europe/Bucharest.
| Name | Required | Description | Default |
|---|---|---|---|
| line | No | Optional line filter. | |
| limit | No | Maximum number of alerts. | |
| stop_id | No | Optional STPT stop ID. | |
| active_only | No | Return only alerts active at the current instant. |
Output Schema
| Name | Required | Description |
|---|---|---|
| line | No | |
| count | Yes | |
| error | No | Structured failure, if any. |
| alerts | No | |
| status | Yes | Overall result status. |
| stop_id | No | |
| provenance | No | Source and freshness metadata. |
| active_only | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, openWorld, and non-destructive, so the safety profile is covered. The description adds genuinely useful non-obvious context: that active-period evaluation happens in Europe/Bucharest and that both current and historical alerts are returned. It stops short of noting data freshness or rate limits.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight sentences, front-loaded with the core action, and the timezone note earns its place as non-obvious behavior. No filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema, full schema coverage, and rich annotations, the description covers what the agent needs to call it correctly. Only the sibling relationship to get_service_status is unaddressed, which is a minor gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all four parameters are already documented. The description reinforces that line and stop_id are optional filters ('when supplied') and adds timezone context for active_only, but supplies no format or semantics the schema lacks. Baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Get ... STPT service alerts') and clarifies the current-vs-historical scope. It is distinguishable from most siblings like get_arrivals or get_vehicles, though it never explicitly contrasts with get_service_status, which is the nearest sibling and could be confused with it.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
'Filters by stop ID and/or line when supplied' implies how to narrow results but gives no explicit when-to-use guidance or exclusions relative to get_service_status or get_arrivals. The agent must infer that this is the tool for alert text as opposed to arrival/status data.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_arrivalsGet STPT arrivalsARead-onlyIdempotent
Get upcoming arrivals at an STPT stop. Live data is used only when its freshness is verified; otherwise departures come from the bundled GTFS timetable. Check provenance.freshness, provenance.fallback_reason, and each arrival's live flag.
| Name | Required | Description | Default |
|---|---|---|---|
| line | No | Optional line filter such as '2', 'E3', or 'M32'. | |
| limit | No | Maximum number of arrivals to return. | |
| stop_id | Yes | Numeric STPT stop ID. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | |
| error | No | Structured failure, if any. |
| status | Yes | Overall result status. |
| stop_id | Yes | |
| arrivals | No | |
| stop_name | Yes | |
| provenance | No | Source and freshness metadata. |
| line_filter | No | |
| lookahead_minutes | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover read-only/idempotent safety, and the description adds genuinely new behavior: live data only when freshness is verified, GTFS fallback otherwise, and which fields reveal provenance. That is meaningful context beyond the structured hints, though it doesn't mention rate limits or ordering guarantees.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences, front-loaded with the purpose, then the data-source caveat, then the fields to check. No sentence is redundant.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return values need not be explained, yet the description helpfully points at the exact provenance fields an agent should inspect. Nothing necessary for correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so stop_id, line, and limit are fully documented in the schema. The description adds no syntax, format, or defaulting detail beyond that, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Get upcoming arrivals at an STPT stop'), which clearly separates it from get_stop, get_vehicles, and the other siblings. It doesn't explicitly name an alternative tool to route away to, but the resource is unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies the use case (fetching arrivals for a stop) but gives no explicit when-to-use vs when-not guidance and never references sibling tools like get_stop or get_vehicles. The fallback explanation is behavioral rather than selection guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_service_statusGet STPT MCP service statusARead-onlyIdempotent
Report server readiness, version, bundled network feed metadata, and bounded cache statistics. Contains no secrets.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Structured failure, if any. |
| ready | Yes | |
| caches | No | |
| status | Yes | Overall result status. |
| version | Yes | |
| timezone | Yes | |
| live_feed | No | Freshness of the live arrivals feed. Null until a stop has been fetched. |
| provenance | No | Source and freshness metadata. |
| started_at | No | |
| current_time | Yes | |
| network_feed_version | Yes | |
| network_generated_at | Yes | |
| network_service_range | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, non-destructive, and openWorld, so the safety profile is covered. The description adds genuine context beyond them: that the response includes bundled feed metadata and 'bounded' cache statistics, plus the assurance 'Contains no secrets,' which matters for an externally exposed status endpoint.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences with no filler, and the primary payload (what is reported) is front-loaded before the safety note.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-value details need not be in the description, and the annotations cover the safety contract. For a zero-parameter read-only status tool, nothing an agent needs in order to call it is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so the baseline is 4; there is nothing for the description to disambiguate. It correctly avoids inventing parameter semantics that do not exist.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Uses a specific verb ('Report') with a concrete list of resources: readiness, version, bundled network feed metadata, and bounded cache statistics. This is unmistakably a service-health tool and cannot be confused with any of the transit-data siblings such as get_vehicles or get_alerts.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The stated contents imply a health/diagnostics use case, but the description never says when to call it, whether it should be polled, or how it relates to the sibling tools. Since no sibling covers the same ground, the missing guidance is less costly, leaving it at implied-only usage.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_stopGet STPT stop detailsARead-onlyIdempotent
Get coordinates, serving lines, directions, terminals, and neighboring stops for a numeric STPT stop ID.
| Name | Required | Description | Default |
|---|---|---|---|
| stop_id | Yes | Numeric STPT stop ID. |
Output Schema
| Name | Required | Description |
|---|---|---|
| stop | No | |
| error | No | Structured failure, if any. |
| status | Yes | Overall result status. |
| provenance | No | Source and freshness metadata. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, openWorldHint, and destructiveHint=false, so the safety profile is fully covered. The description adds useful scope information by naming the returned facets (including neighboring stops, a non-obvious extra), but says nothing about behavior on invalid IDs or how fresh/complete the data is.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One tightly packed sentence: the action leads, the returned fields are enumerated, and the input constraint trails. Zero filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema covering return shape and annotations covering safety, the description is nearly complete. The only residual gap is how an agent should obtain a valid stop ID (e.g., via search_stops), which would round it out.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and the single parameter is documented as 'Numeric STPT stop ID.' The description restates the same constraint without adding format, range, or sourcing guidance, so baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('Get ... stop details') and enumerates exactly what is returned: coordinates, serving lines, directions, terminals, and neighboring stops, keyed by a numeric STPT stop ID. This distinguishes it well from siblings like get_arrivals, though it does not explicitly contrast it with search_stops.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied by the 'numeric STPT stop ID' phrasing — an agent can infer it is the lookup-by-ID tool versus search_stops' query-based discovery, but no when/when-not or alternative is named. No prerequisites or failure conditions for an unknown ID are mentioned.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_vehiclesGet STPT vehicle positionsARead-onlyIdempotent
Get current STPT vehicle positions, optionally filtered by line. Each vehicle includes its upstream observation timestamp and staleness flag.
| Name | Required | Description | Default |
|---|---|---|---|
| line | No | Optional line filter. | |
| limit | No | Maximum number of vehicles. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | |
| error | No | Structured failure, if any. |
| status | Yes | Overall result status. |
| vehicles | No | |
| provenance | No | Source and freshness metadata. |
| line_filter | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish this as a safe, read-only, idempotent operation, so the description doesn't need to cover safety. It adds useful behavioral context by disclosing that each vehicle carries an upstream observation timestamp and staleness flag, which tells the agent how to interpret data freshness. It does not mention pagination or limit behavior, but that is a minor gap given the annotations and output schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two concise sentences, front-loaded with the purpose and followed by the optional filter and data-quality note. No filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool is simple: two optional parameters, full schema coverage, annotations covering safety and openness, and an output schema that documents return values. The description is complete enough for correct invocation. It could be improved by noting when to prefer this over get_arrivals or other siblings, but that is outside the core completeness of this definition.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both the 'line' filter and 'limit' cap are already documented in the schema. The description only restates the optional line filter and adds no syntax, format, or default details beyond the schema. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('Get') and resource ('current STPT vehicle positions'), making it immediately distinguishable from siblings like search_stops, list_routes, and get_arrivals. No tautology or ambiguity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It implies usage through 'current' positions and an optional line filter, but gives no explicit when-to-use/when-not guidance, no alternatives among the sibling tools, and no prerequisites. The reader must infer the role of this tool from the resource name alone.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_routesList STPT routesBRead-onlyIdempotent
List STPT tram, trolleybus, bus, metropolitan, express, and vaporetto routes from the bundled GTFS-derived network snapshot.
| Name | Required | Description | Default |
|---|---|---|---|
| mode | No | Optional mode filter. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | |
| error | No | Structured failure, if any. |
| routes | No | |
| status | Yes | Overall result status. |
| provenance | No | Source and freshness metadata. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare read-only, idempotent, non-destructive behavior, so the safety profile is covered. The description adds one genuinely useful fact beyond that: results come from a static bundled GTFS-derived snapshot rather than live data. It says nothing about result size, ordering, or pagination.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with the resource and data source in the opening clause. It is terse and free of filler, though the mode list it adds is both redundant with the schema and inaccurate.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-value explanation is not required, and there are no required parameters. What is missing is any guidance on filtering semantics or on the static-vs-live distinction's implications, plus the inaccurate mode list, leaving the definition only minimally sufficient.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% for the single optional `mode` parameter, which sets a baseline of 3, but the description names modes ("metropolitan, express") that are not valid values in the VehicleMode enum (tram, trolleybus, bus, vaporetto, unknown). This misleads the agent rather than adding meaning, so it scores below baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("List STPT ... routes") and scopes it to the bundled GTFS-derived snapshot, which clearly separates it from siblings like search_stops or get_vehicles. It does not explicitly name an alternative, so it stops short of full sibling differentiation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No statement of when to call this versus search_stops, get_stop, or get_service_status, and no guidance on when the mode filter should be applied. The agent must infer usage entirely from the name and the mode enumeration.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_stopsSearch STPT stopsARead-onlyIdempotent
Search the bundled STPT network by stop ID, stop name, or line. Uses diacritic-insensitive matching. Returns up to 50 stops.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum number of stops to return. | |
| query | Yes | Stop code, stop name fragment, or line number. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | |
| error | No | Structured failure, if any. |
| query | Yes | |
| stops | No | |
| status | Yes | Overall result status. |
| provenance | No | Source and freshness metadata. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare read-only, idempotent, non-destructive and open-world. The description adds genuine behavioral detail beyond them: diacritic-insensitive matching and a hard result cap of 50, both of which affect how an agent should query and interpret results.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences with no filler, and the core action and scope are front-loaded before the matching and result-count details.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present and full parameter documentation, the description covers what is needed to call the tool correctly. The only omission is how the tool relates to the sibling get_stop for exact-ID lookups.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both query and limit are already documented with examples and bounds. The description's mention of 'up to 50 stops' and the match types only restates schema-level facts, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb (Search) plus resource (STPT stops) and explicit scope ('bundled STPT network'). It also names the three match keys, so an agent can immediately tell this apart from get_stop (single-stop lookup) and list_routes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage through the set of searchable keys (stop ID, name, line), which lets an agent infer when to reach for it. However, it never explicitly says when to prefer it over the sibling get_stop, nor any exclusions or prerequisites.
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.
7 tool updates
v0.3.7- Changed
get_alerts5 fields changed- added
Output schema / $defs / FreshnessAdded value: +{ + "enum": [ + "fresh", + "stale", + "unverified" + ], + "title": "Freshness", + "type": "string" +} - changed
Output schema / $defs / Provenance / properties / data_age_seconds / descriptionPrevious value: -"Age of the source/cache entry when returned."New value: +"Age of the upstream observation when returned, in seconds. Null when the age cannot be measured." - added
Output schema / $defs / Provenance / properties / freshnessAdded value: +{ + "$ref": "#/$defs/Freshness", + "default": "unverified", + "description": "fresh: an upstream observation time was checked against this server's clock and is within tolerance. stale: the check failed (old, clock-skewed, or frozen data) or a cache entry past its TTL is served. unverified: the data carries no upstream clock reference (schedules, the network snapshot, alerts), so it cannot be confirmed as current." +} - changed
Output schema / $defs / Provenance / properties / is_stale / descriptionPrevious value: -"True when the stale cache policy supplied the data."New value: +"True when stale data is being returned, whether from cache or upstream." - changed
Output schema / $defs / Provenance / properties / observed_at / descriptionPrevious value: -"Upstream observation timestamp. Live arrivals expose no timestamp, so this is the fetch time; schedules describe future service and leave it null; vehicles use their upstream timestamp."New value: +"Upstream observation timestamp in UTC. Live arrivals derive it from the feed's own display times and remaining minutes; it is null when the feed carries no clock reference or is rejected as stale. Schedules describe future service and leave it null; vehicles use their upstream timestamp."
- Changed
get_arrivals5 fields changed- added
Output schema / $defs / FreshnessAdded value: +{ + "enum": [ + "fresh", + "stale", + "unverified" + ], + "title": "Freshness", + "type": "string" +} - changed
Output schema / $defs / Provenance / properties / data_age_seconds / descriptionPrevious value: -"Age of the source/cache entry when returned."New value: +"Age of the upstream observation when returned, in seconds. Null when the age cannot be measured." - added
Output schema / $defs / Provenance / properties / freshnessAdded value: +{ + "$ref": "#/$defs/Freshness", + "default": "unverified", + "description": "fresh: an upstream observation time was checked against this server's clock and is within tolerance. stale: the check failed (old, clock-skewed, or frozen data) or a cache entry past its TTL is served. unverified: the data carries no upstream clock reference (schedules, the network snapshot, alerts), so it cannot be confirmed as current." +} - changed
Output schema / $defs / Provenance / properties / is_stale / descriptionPrevious value: -"True when the stale cache policy supplied the data."New value: +"True when stale data is being returned, whether from cache or upstream." - changed
Output schema / $defs / Provenance / properties / observed_at / descriptionPrevious value: -"Upstream observation timestamp. Live arrivals expose no timestamp, so this is the fetch time; schedules describe future service and leave it null; vehicles use their upstream timestamp."New value: +"Upstream observation timestamp in UTC. Live arrivals derive it from the feed's own display times and remaining minutes; it is null when the feed carries no clock reference or is rejected as stale. Schedules describe future service and leave it null; vehicles use their upstream timestamp."
- Changed
get_service_status7 fields changed- added
Output schema / $defs / FreshnessAdded value: +{ + "enum": [ + "fresh", + "stale", + "unverified" + ], + "title": "Freshness", + "type": "string" +} - added
Output schema / $defs / LiveFeedStatusAdded value: +{ + "additionalProperties": false, + "properties": { + "assessed_at": { + "description": "When that fetch was assessed, in UTC.", + "format": "date-time", + "title": "Assessed At", + "type": "string" + }, + "clock_unchanged_seconds": { + "description": "How long that stop's feed clock has stood still while real time advanced.", + "minimum": 0, + "title": "Clock Unchanged Seconds", + "type": "number" + }, + "freshness": { + "$ref": "#/$defs/Freshness" + }, + "recent_stops": { + "additionalProperties": { + "type": "integer" + }, + "description": "Freshness of every stop fetched in the last 15 minutes, counted.", + "propertyNames": { + "$ref": "#/$defs/Freshness" + }, + "title": "Recent Stops", + "type": "object" + }, + "skew_seconds": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The feed's implied clock minus the real clock, in seconds. Positive means the feed runs ahead. Null when the feed carried no clock reference.", + "title": "Skew Seconds" + }, + "stop_id": { + "description": "Stop of the most recent live arrivals fetch.", + "title": "Stop Id", + "type": "string" + } + }, + "required": [ + "stop_id", + "assessed_at", + "freshness", + "clock_unchanged_seconds" + ], + "title": "LiveFeedStatus", + "type": "object" +} - changed
Output schema / $defs / Provenance / properties / data_age_seconds / descriptionPrevious value: -"Age of the source/cache entry when returned."New value: +"Age of the upstream observation when returned, in seconds. Null when the age cannot be measured." - added
Output schema / $defs / Provenance / properties / freshnessAdded value: +{ + "$ref": "#/$defs/Freshness", + "default": "unverified", + "description": "fresh: an upstream observation time was checked against this server's clock and is within tolerance. stale: the check failed (old, clock-skewed, or frozen data) or a cache entry past its TTL is served. unverified: the data carries no upstream clock reference (schedules, the network snapshot, alerts), so it cannot be confirmed as current." +} - changed
Output schema / $defs / Provenance / properties / is_stale / descriptionPrevious value: -"True when the stale cache policy supplied the data."New value: +"True when stale data is being returned, whether from cache or upstream." - changed
Output schema / $defs / Provenance / properties / observed_at / descriptionPrevious value: -"Upstream observation timestamp. Live arrivals expose no timestamp, so this is the fetch time; schedules describe future service and leave it null; vehicles use their upstream timestamp."New value: +"Upstream observation timestamp in UTC. Live arrivals derive it from the feed's own display times and remaining minutes; it is null when the feed carries no clock reference or is rejected as stale. Schedules describe future service and leave it null; vehicles use their upstream timestamp." - added
Output schema / properties / live_feedAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/LiveFeedStatus" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Freshness of the live arrivals feed. Null until a stop has been fetched." +}
- Changed
get_stop5 fields changed- added
Output schema / $defs / FreshnessAdded value: +{ + "enum": [ + "fresh", + "stale", + "unverified" + ], + "title": "Freshness", + "type": "string" +} - changed
Output schema / $defs / Provenance / properties / data_age_seconds / descriptionPrevious value: -"Age of the source/cache entry when returned."New value: +"Age of the upstream observation when returned, in seconds. Null when the age cannot be measured." - added
Output schema / $defs / Provenance / properties / freshnessAdded value: +{ + "$ref": "#/$defs/Freshness", + "default": "unverified", + "description": "fresh: an upstream observation time was checked against this server's clock and is within tolerance. stale: the check failed (old, clock-skewed, or frozen data) or a cache entry past its TTL is served. unverified: the data carries no upstream clock reference (schedules, the network snapshot, alerts), so it cannot be confirmed as current." +} - changed
Output schema / $defs / Provenance / properties / is_stale / descriptionPrevious value: -"True when the stale cache policy supplied the data."New value: +"True when stale data is being returned, whether from cache or upstream." - changed
Output schema / $defs / Provenance / properties / observed_at / descriptionPrevious value: -"Upstream observation timestamp. Live arrivals expose no timestamp, so this is the fetch time; schedules describe future service and leave it null; vehicles use their upstream timestamp."New value: +"Upstream observation timestamp in UTC. Live arrivals derive it from the feed's own display times and remaining minutes; it is null when the feed carries no clock reference or is rejected as stale. Schedules describe future service and leave it null; vehicles use their upstream timestamp."
- Changed
get_vehicles5 fields changed- added
Output schema / $defs / FreshnessAdded value: +{ + "enum": [ + "fresh", + "stale", + "unverified" + ], + "title": "Freshness", + "type": "string" +} - changed
Output schema / $defs / Provenance / properties / data_age_seconds / descriptionPrevious value: -"Age of the source/cache entry when returned."New value: +"Age of the upstream observation when returned, in seconds. Null when the age cannot be measured." - added
Output schema / $defs / Provenance / properties / freshnessAdded value: +{ + "$ref": "#/$defs/Freshness", + "default": "unverified", + "description": "fresh: an upstream observation time was checked against this server's clock and is within tolerance. stale: the check failed (old, clock-skewed, or frozen data) or a cache entry past its TTL is served. unverified: the data carries no upstream clock reference (schedules, the network snapshot, alerts), so it cannot be confirmed as current." +} - changed
Output schema / $defs / Provenance / properties / is_stale / descriptionPrevious value: -"True when the stale cache policy supplied the data."New value: +"True when stale data is being returned, whether from cache or upstream." - changed
Output schema / $defs / Provenance / properties / observed_at / descriptionPrevious value: -"Upstream observation timestamp. Live arrivals expose no timestamp, so this is the fetch time; schedules describe future service and leave it null; vehicles use their upstream timestamp."New value: +"Upstream observation timestamp in UTC. Live arrivals derive it from the feed's own display times and remaining minutes; it is null when the feed carries no clock reference or is rejected as stale. Schedules describe future service and leave it null; vehicles use their upstream timestamp."
- Changed
list_routes5 fields changed- added
Output schema / $defs / FreshnessAdded value: +{ + "enum": [ + "fresh", + "stale", + "unverified" + ], + "title": "Freshness", + "type": "string" +} - changed
Output schema / $defs / Provenance / properties / data_age_seconds / descriptionPrevious value: -"Age of the source/cache entry when returned."New value: +"Age of the upstream observation when returned, in seconds. Null when the age cannot be measured." - added
Output schema / $defs / Provenance / properties / freshnessAdded value: +{ + "$ref": "#/$defs/Freshness", + "default": "unverified", + "description": "fresh: an upstream observation time was checked against this server's clock and is within tolerance. stale: the check failed (old, clock-skewed, or frozen data) or a cache entry past its TTL is served. unverified: the data carries no upstream clock reference (schedules, the network snapshot, alerts), so it cannot be confirmed as current." +} - changed
Output schema / $defs / Provenance / properties / is_stale / descriptionPrevious value: -"True when the stale cache policy supplied the data."New value: +"True when stale data is being returned, whether from cache or upstream." - changed
Output schema / $defs / Provenance / properties / observed_at / descriptionPrevious value: -"Upstream observation timestamp. Live arrivals expose no timestamp, so this is the fetch time; schedules describe future service and leave it null; vehicles use their upstream timestamp."New value: +"Upstream observation timestamp in UTC. Live arrivals derive it from the feed's own display times and remaining minutes; it is null when the feed carries no clock reference or is rejected as stale. Schedules describe future service and leave it null; vehicles use their upstream timestamp."
- Changed
search_stops5 fields changed- added
Output schema / $defs / FreshnessAdded value: +{ + "enum": [ + "fresh", + "stale", + "unverified" + ], + "title": "Freshness", + "type": "string" +} - changed
Output schema / $defs / Provenance / properties / data_age_seconds / descriptionPrevious value: -"Age of the source/cache entry when returned."New value: +"Age of the upstream observation when returned, in seconds. Null when the age cannot be measured." - added
Output schema / $defs / Provenance / properties / freshnessAdded value: +{ + "$ref": "#/$defs/Freshness", + "default": "unverified", + "description": "fresh: an upstream observation time was checked against this server's clock and is within tolerance. stale: the check failed (old, clock-skewed, or frozen data) or a cache entry past its TTL is served. unverified: the data carries no upstream clock reference (schedules, the network snapshot, alerts), so it cannot be confirmed as current." +} - changed
Output schema / $defs / Provenance / properties / is_stale / descriptionPrevious value: -"True when the stale cache policy supplied the data."New value: +"True when stale data is being returned, whether from cache or upstream." - changed
Output schema / $defs / Provenance / properties / observed_at / descriptionPrevious value: -"Upstream observation timestamp. Live arrivals expose no timestamp, so this is the fetch time; schedules describe future service and leave it null; vehicles use their upstream timestamp."New value: +"Upstream observation timestamp in UTC. Live arrivals derive it from the feed's own display times and remaining minutes; it is null when the feed carries no clock reference or is rejected as stale. Schedules describe future service and leave it null; vehicles use their upstream timestamp."
7 tool updates
v0.2.0- First observed
get_alerts - First observed
get_arrivals - First observed
get_service_status - First observed
get_stop - First observed
get_vehicles - First observed
list_routes - First observed
search_stops
TDQS
Scored across 7 tools
Each tool targets a distinct transit object or concern: stop search, stop detail, route listing, arrivals, service status, alerts, and vehicle positions. Boundaries are clear even where arrivals and vehicle positions both relate to live transit data.
All tool names use a consistent snake_case verb_noun convention: search_stops, get_stop, list_routes, get_arrivals, get_service_status, get_alerts, get_vehicles. No ambiguous or inconsistent naming styles appear.
Seven tools is well-scoped for a transit MCP server, covering discovery, detail, arrivals, alerts, vehicles, and health without bloat. Each tool has a clear role.
The surface covers core transit workflows: finding stops, viewing routes and stop details, checking arrivals, alerts, and vehicle positions. Minor gaps exist, such as explicit route-detail or trip-planning operations, but core use cases are supported.
Maintenance
Related MCP Connectors
Transitland MCP — global GTFS aggregator
Read-only public transit departures, stop search, and city coverage for bus and train users.
Official MCP server for MAQAMI, a hotel and flight booking platform with 3M+ hotels. Search live hotel rates and flights, look up places, airports and hotel details, then prebook and book. Remote Streamable HTTP endpoint, no API key required.
Lviv public transport MCP: stops, timetables, routes, and live vehicle positions. No API key.
Related MCP Servers
- AlicenseAqualityCmaintenanceMCP server for Chelaile realtime public transit data in China, including bus and metro arrivals, vehicle positions, nearby stops, timetables, and route planning. It runs locally over stdio with no login or account configuration required.15199 npm23MIT
- AlicenseNot gradedqualityAmaintenanceEnables querying stops, routes, real-time arrivals, vehicle positions, and schedules from OneBusAway transit APIs via MCP, supporting STDIO or Streamable HTTP.88 npm1Apache 2.0
- AlicenseAqualityCmaintenancePrivate MCP server adapter for RedTransporteAPI, providing read-only access to Santiago de Chile public transit data including stops, routes, real-time iBus/RED predictions, and RAPTOR journey planning, secured with deny-by-default bearer token authentication over stdio or streamable HTTP.151GPL 3.0
- AlicenseNot gradedqualityDmaintenanceEnables querying real-time BART and SF Muni transit data, including departures, trip planning, fares, advisories, routes, alerts, vehicle positions, and schedules, from any MCP-compatible client.1MIT