Metadata-Version: 2.5
Name: stpt-mcp
Version: 0.3.7
Summary: An unofficial MCP server for STPT public transport in Timișoara
Project-URL: Homepage, https://github.com/vladirocox/stpt-mcp
Project-URL: Repository, https://github.com/vladirocox/stpt-mcp
Project-URL: Issues, https://github.com/vladirocox/stpt-mcp/issues
Project-URL: Changelog, https://github.com/vladirocox/stpt-mcp/blob/main/CHANGELOG.md
Author-email: Vlad Ohai <vladirocox@users.noreply.github.com>
License: MIT License
        
        Copyright (c) 2026 Vlad Ohai
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
License-File: NOTICE
Keywords: bus,mcp,stpt,timisoara,tram,transit
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.28.1
Requires-Dist: mcp<3,>=2.3.0
Requires-Dist: pydantic<3,>=2.12
Requires-Dist: uvicorn<1,>=0.31.1
Description-Content-Type: text/markdown

# STPT MCP

[![CI](https://github.com/vladirocox/stpt-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/vladirocox/stpt-mcp/actions/workflows/ci.yml)

An unofficial, standalone [Model Context Protocol](https://modelcontextprotocol.io/)
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

![An assistant answering "when does the 14 get to Piata Marasti?" with the stpt MCP tools](https://raw.githubusercontent.com/vladirocox/stpt-mcp/main/assets/chat-arrivals.png)

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.

## 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-After`
  support, 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](https://docs.astral.sh/uv/) for development (optional for end users).
- Network access to STPT endpoints for live data. Discovery tools work offline.

## Install

From [PyPI](https://pypi.org/project/stpt-mcp/), with one command:

```bash
pip install stpt-mcp
stpt-mcp --version
```

Or run it without installing anything permanently, using
[uv](https://docs.astral.sh/uv/):

```bash
uvx stpt-mcp --version
```

From a checkout:

```bash
uv sync --all-groups
uv run stpt-mcp --version
```

Or from a wheel/sdist:

```bash
python -m venv .venv
. .venv/bin/activate
pip install dist/stpt_mcp-0.3.7-py3-none-any.whl
stpt-mcp --version
```

`stpt-mcp` with no subcommand is equivalent to `stpt-mcp serve` and defaults to
stdio.

## Quick start

### stdio

```bash
uv run stpt-mcp serve --transport stdio
```

The simplest MCP client configuration lets `uvx` fetch the package from PyPI
on first use:

```json
{
  "mcpServers": {
    "stpt": {
      "command": "uvx",
      "args": ["stpt-mcp", "serve", "--transport", "stdio"]
    }
  }
}
```

A minimal MCP client configuration using an installed console script:

```json
{
  "mcpServers": {
    "stpt": {
      "command": "stpt-mcp",
      "args": ["serve", "--transport", "stdio"]
    }
  }
}
```

From a checkout without installing the script:

```json
{
  "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

```bash
uv run stpt-mcp serve --transport http
# endpoint: http://127.0.0.1:8000/mcp
# health:   http://127.0.0.1:8000/healthz
```

Generate a bearer token for any non-local deployment:

```bash
export STPT_MCP_AUTH_TOKEN="$(stpt-mcp token)"
stpt-mcp serve --transport http
```

Clients 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:

```bash
export STPT_MCP_PUBLIC_URL=https://mcp.example.com
export STPT_MCP_OAUTH_PASSWORD='replace-with-a-long-random-password'
stpt-mcp serve --transport http
```

Claude's custom connector can then use **Sign in now** with **Register
automatically**. See [OAuth sign-in](https://github.com/vladirocox/stpt-mcp/blob/main/docs/OAUTH.md) for the reverse-proxy paths
and connector steps.

Minimal client configuration:

```json
{
  "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](https://github.com/vladirocox/stpt-mcp/blob/main/docs/PUBLIC_HTTPS.md),
[OAuth sign-in](https://github.com/vladirocox/stpt-mcp/blob/main/docs/OAUTH.md), [Deployment](https://github.com/vladirocox/stpt-mcp/blob/main/docs/DEPLOYMENT.md), and
[Security](https://github.com/vladirocox/stpt-mcp/blob/main/docs/SECURITY.md).

## Tools

| Tool | Purpose |
| --- | --- |
| `search_stops` | Search stops by ID, name, or line (diacritic-insensitive). |
| `get_stop` | Stop coordinates, lines, directions, destinations, neighbors. |
| `list_routes` | Routes with mode, category, and colors. |
| `get_arrivals` | Live arrivals, with schedule fallback and line/limit filters. |
| `get_alerts` | Service alerts with active-period and stop/line filters. |
| `get_vehicles` | Vehicle positions with observation timestamps and staleness. |
| `get_service_status` | 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:

```json
{
  "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](https://github.com/vladirocox/stpt-mcp/blob/main/docs/TOOLS.md) 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 |
| --- | --- | --- |
| `STPT_MCP_TRANSPORT` | `stdio` | `stdio` or `http`. |
| `STPT_MCP_HOST` | `127.0.0.1` | HTTP bind address. |
| `STPT_MCP_PORT` | `8000` | HTTP port. |
| `STPT_MCP_PATH` | `/mcp` | Streamable HTTP path. |
| `STPT_MCP_LOG_LEVEL` | `INFO` | Python log level. |
| `STPT_MCP_AUTH_TOKEN` | unset | Optional static bearer token (16+ chars). |
| `STPT_MCP_ALLOWED_HOSTS` | loopback | Required for non-loopback binds. |
| `STPT_MCP_ALLOWED_ORIGINS` | loopback | Browser origins allowed by the SDK. |
| `STPT_MCP_MAX_REQUEST_BODY_SIZE` | `1048576` | HTTP request body limit in bytes. |
| `STPT_MCP_MAX_CONCURRENT_HTTP_REQUESTS` | `100` | HTTP concurrency cap. |

TTLs, stale windows, upstream timeouts, retry policy, and cache bounds are also
configurable. See [Configuration](https://github.com/vladirocox/stpt-mcp/blob/main/docs/CONFIGURATION.md) for the complete table.

## How data selection works

1. A fresh cached arrival snapshot for the stop is returned with no request.
2. 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.
3. 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.
4. If the refresh fails, last-known-good data may be served inside the stale
   window.
5. 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

```bash
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 build
```

The 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:

```bash
STPT_MCP_RUN_NETWORK_TESTS=1 uv run pytest -m network
```

See [Testing](https://github.com/vladirocox/stpt-mcp/blob/main/docs/TESTING.md), [Architecture](https://github.com/vladirocox/stpt-mcp/blob/main/docs/ARCHITECTURE.md), and
[Contributing](https://github.com/vladirocox/stpt-mcp/blob/main/CONTRIBUTING.md).

## 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 |
| [STPT Transit](https://github.com/vladirocox/stpt-ha-integration) | Home Assistant | Automations, sensors, and dashboards |
| [Inky pHAT](https://github.com/vladirocox/inkystpt) | 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_unavailable` until a release regenerates them. The feed version and
  service range are exposed by `get_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](https://github.com/vladirocox)).

## License and attribution

MIT licensed, © 2026 Vlad Ohai. STPT data remains the property of its
respective owners. See [LICENSE](https://github.com/vladirocox/stpt-mcp/blob/main/LICENSE) and [NOTICE](https://github.com/vladirocox/stpt-mcp/blob/main/NOTICE).
OpenStreetMap-derived coordinates, if present, are © OpenStreetMap contributors
under the ODbL.
