kitinerary-mcp
by mrwulf
README.md
# kitinerary-mcp
An [MCP](https://modelcontextprotocol.io) server that wraps KDE's [`kitinerary-extractor`](https://invent.kde.org/pim/kitinerary) CLI: give it a travel confirmation email, PDF ticket, or Apple/Google Wallet `.pkpass` file, and it returns whatever structured `schema.org` `Reservation` JSON-LD the file contains — deterministically, with no LLM in the loop.
Most real airline/hotel confirmation emails already embed this JSON-LD (it's the same markup Gmail parses for its own "smart" trip cards). `kitinerary-extractor` reads it straight out, along with structured PDF tickets and Wallet passes that plain text/regex extraction can't touch. This server is a thin, stateless shim around that binary so any MCP client can call it.
## What this is (and isn't)
- **Is**: a single tool, `extract_booking`, that takes a file and returns raw JSON-LD (or nothing, if the extractor finds nothing). No outbound network calls, no secrets, no state — every call is an isolated subprocess against a temp file, cleaned up immediately after.
- **Isn't**: a booking-type mapper. It does not translate the JSON-LD into any particular app's reservation schema — that's a deliberate boundary, so this stays reusable across whatever consumer calls it rather than coupled to one caller's data model.
## How it's used
This server powers the deterministic extraction stage of a self-hosted [TripIt](https://www.tripit.com/) replacement: forward a booking confirmation email, an n8n workflow calls `extract_booking` on it, and a matched result gets mapped and written into [Trek](https://github.com/liketrek/trek) as a trip — no LLM involved for the common case, with an LLM fallback only for emails this server finds nothing in. See [`tripit_replacement.md`](https://github.com/mrwulf/home-cluster/blob/main/docs/tripit_replacement.md) in that pipeline's repo for the full architecture, the JSON-LD-to-booking mapping table this server's output feeds into, and the gotchas found integrating it (some datetime fields come back as `{"@type":"QDateTime",...}` objects rather than plain strings, multi-leg/multi-passenger handling, and more).
That pipeline is one consumer, not a dependency this server has on it — nothing in this repo is coupled to it, and any MCP client can use `extract_booking` the same way. If you build something else on top of this server, opening a PR to list it here is welcome.
## Tool interface
```
extract_booking(file_base64: str, filename: str, context_date: str | None = None) -> {
items: object[], # the raw JSON-LD array kitinerary-extractor emitted (possibly empty)
warnings: string[], # e.g. "unsupported file type", "no reservation data found in file"
}
```
- `file_base64` — the file's raw bytes, base64-encoded (MCP tool args are JSON, so there's no binary/multipart transport at this layer).
- `filename` — used to infer format from the extension. Accepted: `.eml`, `.pdf`, `.pkpass`, `.html`, `.txt`. Anything else is rejected with a warning, not silently passed through.
- `context_date` — optional ISO date/time, forwarded to `kitinerary-extractor --context-date`. Helps resolve dates that don't state a year or are relative to "today" (pass the email's own `Date:` header if you have it).
- No match is **not** an error — you get back `items: []` with a warning. Tool errors are reserved for genuine failures: the binary is missing, the process crashed, or the file exceeds the size cap.
- Limits: 10 MB decoded file size, 60s extraction timeout. Both fail cleanly (a warning, or a tool error) rather than hanging or crashing the caller.
## Running it
```sh
docker run --rm -i ghcr.io/mrwulf/kitinerary-mcp:v0.1.0
```
It speaks MCP over stdio. Wire it into any MCP client's stdio server config, e.g. Claude Desktop's `claude_desktop_config.json`:
```json
{
"mcpServers": {
"kitinerary": {
"command": "docker",
"args": ["run", "--rm", "-i", "ghcr.io/mrwulf/kitinerary-mcp:v0.1.0"]
}
}
}
```
Or point a [ToolHive](https://github.com/stacklok/toolhive) `MCPServer` at the same image with `transport: stdio` — no secrets, no volumes needed.
Always pin an exact tag; `latest` is published alongside each release for convenience but isn't meant to be what you deploy against.
### Tags and version strings
Every release publishes three tags:
- `vX.Y.Z` — this server's own version, e.g. `v0.1.0`. Pin this in normal use.
- `vX.Y.Z-kitineraryA.B.C` — the same build, with the bundled `kitinerary-extractor` release folded into the tag (e.g. `v0.1.0-kitinerary24.12.3`), so you can tell which upstream KDE release a given build carries without pulling it or reading the Dockerfile. Pin this instead if your own compatibility depends on a specific `kitinerary` behavior.
- `latest` — convenience only, not for pinning.
The same two version numbers are also on the image itself, so you don't have to trust the tag alone: as OCI labels (`org.opencontainers.image.version`, `io.github.mrwulf.kitinerary-mcp.kitinerary-version`) readable via `docker inspect`, and in the running server's own MCP `version` field (`0.1.0+kitinerary.24.12.3`, semver build-metadata form) that any MCP client can read after connecting.
## Development
```sh
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pytest
# Fast unit tests (mock the CLI subprocess, no image build needed)
.venv/bin/pytest tests/test_server.py -v
# Real end-to-end tests against the actual kitinerary-extractor binary
docker build -t kitinerary-mcp:test .
IMAGE_TAG=kitinerary-mcp:test .venv/bin/pytest tests/test_integration.py -v
```
## Dependency footprint
`libkitinerary-bin` pulls in a genuine KDE Frameworks 6 + Qt 6 chain (yes, including `libqt6gui6`/`libqt6qml6`, even for this headless CLI use) — there's no slimmer package upstream, so expect a multi-hundred-MB image. This is a tradeoff of using the reference KDE implementation rather than reimplementing extraction logic from scratch, which would mean re-deriving and maintaining parsers for every airline/hotel/rail JSON-LD dialect ourselves.
## License
This repository's own code (`server.py` and supporting files) is [MIT-licensed](LICENSE). The built image additionally includes KDE's `kitinerary` library, which is `LGPL-2.0-or-later` (confirmed from `/usr/share/doc/libkitinerary-bin/copyright` in the Debian trixie package, version `24.12.3-1`). This server only invokes `kitinerary-extractor` as a subprocess — it does not link against `libkitinerary` — so using this image imposes no LGPL obligations on your own application code; the obligations that do apply (source availability for the library itself, etc.) are already satisfied by Debian's own package distribution.
## Keeping this current as kitinerary grows
Two independently-versioned things need tracking, and Renovate handles them differently:
1. **This image's own published tag** — a normal container reference; any downstream consumer's tooling (Renovate, Flux, etc.) tracks it exactly like any other pinned image.
2. **The `libkitinerary-bin`/`libkitinerary-data` apt package versions pinned in the `Dockerfile`** — Renovate has no native Debian-apt datasource, so these are pinned exactly (never a bare `apt-get install` with no version) and annotated for a `customManagers` regex against `KDE/kitinerary`'s GitHub releases as a freshness signal:
```dockerfile
# renovate: depName=KDE/kitinerary datasource=github-releases
ARG KITINERARY_VERSION=24.12.3
```
Debian's version string tracks upstream KDE Gear releases closely but appends its own revision suffix (`-1`, `-2`, ...) that a Renovate bump can't verify still resolves in the `trixie` archive on build day. If it doesn't, the CI build simply fails loudly — an accepted, visible failure mode rather than trying to fully automate around Debian's package cadence.
3. **The Python `mcp` SDK version** — pinned in `requirements.txt`; any pip-aware dependency bot (Renovate, Dependabot) tracks this natively.
## Acceptance test
The fixtures in `tests/fixtures/` and `tests/test_integration.py` cover the smoke test this server is expected to pass before any release: a synthetic hotel confirmation with embedded `LodgingReservation` JSON-LD extracts correctly, a plain marketing email with no structured data returns an empty result with a warning (not an error), and an oversized file is rejected cleanly.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessUnresponsive