KItenerary MCP

Un envoltorio ligero para extraer de manera determinista información de viaje de un documento.

Documentación

kitinerary-mcp

Un servidor MCP que envuelve el CLI kitinerary-extractor de KDE: dale un correo de confirmación de viaje, un ticket PDF o un archivo .pkpass de Apple/Google Wallet, y devuelve el JSON-LD estructurado de schema.org Reservation que contenga el archivo — de forma determinista, sin LLM en el bucle.

La mayoría de los correos de confirmación reales de aerolíneas/hoteles ya incluyen este JSON-LD incrustado (es el mismo marcado que Gmail parsea para sus propias tarjetas de viaje "inteligentes"). kitinerary-extractor lo lee directamente, junto con tickets PDF estructurados y pases de Wallet que la extracción por texto plano/regex no puede tocar. Este servidor es un shim delgado y sin estado alrededor de ese binario, para que cualquier cliente MCP pueda llamarlo.

Qué es (y qué no es)

  • Es: una única herramienta, extract_booking, que recibe un archivo y devuelve JSON-LD crudo (o nada, si el extractor no encuentra nada). Sin llamadas de red salientes, sin secretos, sin estado — cada llamada es un subproceso aislado contra un archivo temporal, limpiado inmediatamente después.
  • No es: un mapeador de tipos de reserva. No traduce el JSON-LD al esquema de reserva de ninguna aplicación en particular — ese es un límite deliberado, para que siga siendo reutilizable en cualquier consumidor que lo llame, en lugar de acoplarse al modelo de datos de un solo llamador.

Cómo se usa

Este servidor impulsa la etapa de extracción determinista de un reemplazo autoalojado de TripIt: reenvía un correo de confirmación de reserva, un flujo de trabajo de n8n llama a extract_booking sobre él, y un resultado coincidente se mapea y escribe en Trek como un viaje — sin LLM para el caso común, con un respaldo de LLM solo para correos donde este servidor no encuentra nada. Consulta tripit_replacement.md en el repositorio de ese pipeline para ver la arquitectura completa, la tabla de mapeo de JSON-LD a reserva que alimenta la salida de este servidor, y los problemas encontrados al integrarlo (algunos campos de fecha/hora vuelven como objetos {"@type":"QDateTime",...} en lugar de cadenas simples, manejo de múltiples tramos/múltiples pasajeros, y más).

Ese pipeline es un consumidor, no una dependencia que este servidor tenga de él — nada en este repositorio está acoplado a él, y cualquier cliente MCP puede usar extract_booking de la misma manera. Si construyes algo más sobre este servidor, abrir un PR para listarlo aquí es bienvenido.

Interfaz de la herramienta

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 — los bytes crudos del archivo, codificados en base64 (los argumentos de las herramientas MCP son JSON, así que no hay transporte binario/multipart en esta capa).
  • filename — se usa para inferir el formato desde la extensión. Aceptados: .eml, .pdf, .pkpass, .html, .txt. Cualquier otra cosa se rechaza con una advertencia, no se pasa silenciosamente.
  • context_date — fecha/hora ISO opcional, reenviada a kitinerary-extractor --context-date. Ayuda a resolver fechas que no indican año o que son relativas a "hoy" (pasa el encabezado Date: del propio correo si lo tienes).
  • Que no haya coincidencia no es un error — recibes items: [] con una advertencia. Los errores de herramienta están reservados para fallos reales: el binario falta, el proceso se bloqueó, o el archivo supera el límite de tamaño.
  • Límites: 10 MB de tamaño de archivo decodificado, 60s de tiempo de espera de extracción. Ambos fallan limpiamente (una advertencia, o un error de herramienta) en lugar de colgarse o bloquear al llamador.

Ejecutarlo

docker run --rm -i ghcr.io/mrwulf/kitinerary-mcp:v0.1.0

Habla MCP sobre stdio. Conéctalo a la configuración del servidor stdio de cualquier cliente MCP, por ejemplo el claude_desktop_config.json de Claude Desktop:

{
  "mcpServers": {
    "kitinerary": {
      "command": "docker",
      "args": ["run", "--rm", "-i", "ghcr.io/mrwulf/kitinerary-mcp:v0.1.0"]
    }
  }
}

O apunta un MCPServer de ToolHive a la misma imagen con transport: stdio — sin secretos, sin volúmenes necesarios.

Siempre fija una etiqueta exacta; latest se publica junto con cada release por conveniencia, pero no está pensada para desplegar contra ella.

Etiquetas y cadenas de versión

Cada release publica tres etiquetas:

  • vX.Y.Z — la versión propia de este servidor, p. ej. v0.1.0. Fija esta en uso normal.
  • vX.Y.Z-kitineraryA.B.C — la misma compilación, con el release de kitinerary-extractor incluido plegado en la etiqueta (p. ej. v0.1.0-kitinerary24.12.3), para que puedas saber qué release de KDE upstream lleva una compilación dada sin descargarla ni leer el Dockerfile. Fija esta en su lugar si tu propia compatibilidad depende de un comportamiento específico de kitinerary.
  • latest — solo por conveniencia, no para fijar.

Los mismos dos números de versión también están en la propia imagen, para que no tengas que confiar solo en la etiqueta: como etiquetas OCI (org.opencontainers.image.version, io.github.mrwulf.kitinerary-mcp.kitinerary-version) legibles mediante docker inspect, y en el campo MCP version del propio servidor en ejecución (0.1.0+kitinerary.24.12.3, forma de metadatos de compilación semver) que cualquier cliente MCP puede leer después de conectarse.

Desarrollo

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

Huella de dependencias

libkitinerary-bin trae una cadena genuina de KDE Frameworks 6 + Qt 6 (sí, incluyendo libqt6gui6/libqt6qml6, incluso para este uso CLI sin interfaz) — no hay un paquete más ligero upstream, así que espera una imagen de varios cientos de MB. Esto es una compensación de usar la implementación de referencia de KDE en lugar de reimplementar la lógica de extracción desde cero, lo que significaría re-derivar y mantener parsers para cada dialecto JSON-LD de aerolínea/hotel/ferrocarril nosotros mismos.

Licencia

El código propio de este repositorio (server.py y archivos de soporte) está bajo licencia MIT. La imagen compilada incluye además la biblioteca kitinerary de KDE, que es LGPL-2.0-or-later (confirmado desde /usr/share/doc/libkitinerary-bin/copyright en el paquete de Debian trixie, versión 24.12.3-1). Este servidor solo invoca a kitinerary-extractor como subproceso — no enlaza contra libkitinerary — así que usar esta imagen no impone obligaciones LGPL a tu propio código de aplicación; las obligaciones que sí aplican (disponibilidad de fuente para la propia biblioteca, etc.) ya están satisfechas por la distribución de paquetes de Debian.

Mantener esto actualizado a medida que kitinerary crece

Dos cosas con versionado independiente necesitan seguimiento, y Renovate las maneja de manera diferente:

  1. La etiqueta publicada de esta propia imagen — una referencia de contenedor normal; cualquier herramienta de consumidor downstream (Renovate, Flux, etc.) la rastrea exactamente como cualquier otra imagen fijada.

  2. Las versiones de paquetes apt libkitinerary-bin/libkitinerary-data fijadas en el Dockerfile — Renovate no tiene fuente de datos nativa para Debian apt, así que estas se fijan exactamente (nunca un apt-get install desnudo sin versión) y se anotan para un regex de customManagers contra los releases de GitHub de KDE/kitinerary como señal de frescura:

    # renovate: depName=KDE/kitinerary datasource=github-releases
    ARG KITINERARY_VERSION=24.12.3
    

    La cadena de versión de Debian sigue de cerca las versiones upstream de KDE Gear pero añade su propio sufijo de revisión (-1, -2, ...) que un bump de Renovate no puede verificar que siga resolviéndose en el archivo trixie el día de la compilación. Si no lo hace, la compilación CI simplemente falla ruidosamente — un modo de fallo aceptado y visible en lugar de intentar automatizar completamente alrededor del ritmo de paquetes de Debian.

  3. La versión del SDK de Python mcp — fijada en requirements.txt; cualquier bot de dependencias consciente de pip (Renovate, Dependabot) rastrea esto de forma nativa.

Prueba de aceptación

Los fixtures en tests/fixtures/ y tests/test_integration.py cubren la prueba de humo que este servidor debe pasar antes de cualquier release: una confirmación de hotel sintética con JSON-LD LodgingReservation incrustado se extrae correctamente, un correo de marketing plano sin datos estructurados devuelve un resultado vacío con una advertencia (no un error), y un archivo sobredimensionado se rechaza limpiamente.