cnmc-logistica
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., "@cnmc-logistica Compara los precios de almacenamiento de Repsol y Cepsa en 2025"
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.
mcp-logistica-hidrocarburos-cnmc
Histórico de la logística de productos petrolíferos que publica la CNMC, consultable en lenguaje natural a través de un servidor MCP de solo lectura.
Aviso legal. Proyecto no oficial, no afiliado a la CNMC ni a ningún operador. Los datos son públicos y los publica la CNMC en hidrocarburos.cnmc.es. Este repositorio los copia y los estructura tal cual, sin garantía de exactitud ni de actualización. Para cualquier decisión, consulte la fuente original.
Qué es
La CNMC publica las condiciones de acceso a las instalaciones de almacenamiento y transporte de productos petrolíferos de todos los operadores: precios, mermas, servicios incluidos, rutas origen-destino y capacidades. El portal solo muestra el estado actual y cada operador redacta sus servicios a su manera.
Este proyecto:
Descarga una vez al mes todo el portal (unas 600 páginas y PDF), con educación: una petición por segundo como máximo,
robots.txten cada ejecución y un User-Agent identificable.Guarda el crudo tal cual (
raw/AAAA-MM-DD/, con sha256 y manifiesto) y nunca borra nada: cada cambio de un precio o de una condición se conserva como una versión nueva.Normaliza lo justo para poder comparar: categoría de servicio, tipo de precio, unidad, qué incluye cada servicio.
Lo expone mediante un servidor MCP que solo lee una base SQLite. El servidor nunca hace scraping.
Con la foto del 11/11/2026: 115 instalaciones (71 de almacenamiento y 44 aeroportuarias), 258 detalles (un producto en una instalación), 31 operadores con instalaciones, 1.579 precios de las páginas, 2.163 rutas origen-destino y 27 PDF leídos por completo (66 precios de tarifas, 2.495 celdas de matrices, 18 bonificaciones, 21 condiciones con cantidad y 365 capacidades).
Related MCP server: gasolineras-mcp
Cómo funciona
flowchart LR
portal[("hidrocarburos.cnmc.es")]
subgraph ETL["ETL (mensual, GitHub Actions)"]
descubrir["descubrir<br/>mapas + buscador"]
actualizar["actualizar<br/>fichas, detalles,<br/>origen-destino, PDF"]
raw[("raw/AAAA-MM-DD/<br/>bytes + sha256 + manifiesto")]
parsers["parsers<br/>(guiados por etiquetas de texto)"]
end
bd[("SQLite con historial<br/>data/cnmc_logistica.db")]
mcp["servidor MCP<br/>solo lectura"]
cliente["Claude u otro<br/>cliente MCP"]
portal --> descubrir --> actualizar --> raw --> parsers --> bd
raw -. "reconstruir (sin red)" .-> bd
bd --> mcp --> cliente
config[/"config/*.yaml<br/>correspondencias revisables"/] --> parsersETL y servidor están separados. El servidor no importa ni una línea del descargador o de los parsers (hay un test que lo comprueba).
El crudo es la fuente de verdad. La base se puede regenerar entera desde
raw/conreconstruir; por eso un fallo de interpretación nunca pierde información.Nada aborta por contenido inesperado. Un bloque o un PDF que no se entiende se anota en la tabla
incidencias(recurso, operador y texto), se conserva su crudo y se sigue. La orden termina con código distinto de cero si hubo errores, para que el workflow se marque en rojo.
Instalación
Requiere uv (instala Python 3.12 por su cuenta).
git clone https://github.com/mariogmg/mcp-logistica-hidrocarburos-cnmc
cd mcp-logistica-hidrocarburos-cnmc
uv sync
uv run cnmc-logistica infoEl repositorio incluye la base de datos ya cargada (data/cnmc_logistica.db), así que el servidor
funciona sin descargar nada.
Uso
Consultar los datos desde Claude Desktop
Añade a la configuración de Claude Desktop el bloque de ejemplos/claude_desktop_config.json (cambiando la ruta) y reinicia. Más detalles, incluido el transporte HTTP, en ejemplos/LEEME.md.
Órdenes de la CLI
Orden | Qué hace | ¿Usa la red? |
| Mapas + buscador → | Sí |
| Descarga todo el inventario a | Sí |
| Crea una base nueva a partir de todas las fotos de | No |
| Arranca el servidor MCP ( | No |
| Versión, fotos disponibles y filas de cada tabla | No |
uv run cnmc-logistica descubrir # ~1 min si ya hay foto del día; ~4 min desde cero
uv run cnmc-logistica actualizar # ~10 min desde cero (1 petición por segundo)
uv run cnmc-logistica reconstruir --salida /tmp/nueva.db
uv run cnmc-logistica servir --transporte streamable-http --puerto 8000Variables de entorno del servidor HTTP: CNMC_MCP_TOKEN (si se define, se exige como token
Bearer) y CNMC_MCP_HOSTS_PERMITIDOS (cabeceras Host admitidas, separadas por comas; por
defecto solo local). CNMC_BD cambia la ruta de la base.
Docker
docker build -t cnmc-logistica .
docker run --rm -p 8000:8000 cnmc-logistica # MCP en http://localhost:8000/mcpHerramientas MCP
Herramienta | Para qué |
| Última foto, volumen de datos, incidencias y esquema de tablas |
| Operadores con su nº de instalaciones por tipo y nombres anteriores |
| Filtros por operador, tipo, comunidad y producto |
| Ficha completa: detalles, precios, condiciones y capacidad |
| Precios vigentes (de la página y de los PDF, con su |
| Compara una categoría entre instalaciones y operadores, solo entre comparables |
| Todas las versiones de los precios de un detalle |
| Coste de llevar producto de un origen a una instalación |
| Capacidad (miles de m³) por compañía, instalación y producto |
| Matrices de los PDF: reconsignación entre instalaciones, consignación de éster y origen-destino |
| Bonificaciones y compensaciones de los contratos a largo plazo |
| Un |
Ejemplos de preguntas
¿Cuántos operadores hay y cuántas instalaciones tiene cada uno?
¿Qué cuota de instalaciones tiene Exolum por tipo?
Compara el precio de almacenamiento de queroseno en aeropuertos, separando «almacenamiento» de «servicios aeroportuarios».
¿Qué operadores ofrecen almacenamiento en Canarias y a qué precio?
¿Qué mermas aplican los distintos operadores al gasóleo?
¿Cuánto cuesta llevar producto desde Huelva hasta Cartagena?
¿Ha cambiado el precio de las transferencias de Exolum en Puxeiros?
¿Cuánto cuesta reconsignar gasóleo de Huelva a Zaragoza?
¿Qué bonificaciones tienen los contratos a largo plazo de queroseno?
¿Cuánto cobra DISA por los servicios logísticos de gasolinas en cada isla?
Modelo de datos
Dos niveles de identificador, los mismos que usa el portal: instalación (id_tanque) y
detalle (id_tanque_instalacion, un producto dentro de una instalación). Los precios cuelgan
del detalle, y cada detalle tiene su propio titular.
Tabla | Contenido |
| Código estable del buscador e historial de sus nombres |
| Tipo (almacenamiento / aeroportuaria) y comunidad |
| Titular, conexión a oleoducto y productos normalizados |
| Un valor por fila, con |
| Mermas y demás condiciones (valor numérico si lo hay, y texto) |
| Lista «Incluye:» de cada servicio principal |
| Rutas ( |
| PDF global de capacidades, enlazado a |
| Metadatos y tablas genéricas de cada PDF |
| Qué línea de precios de qué detalle remite a qué PDF |
| Tarifas de los PDF (concepto → precio), con categoría, unidad y año |
| Matrices origen × destino de los PDF (vista |
| Bonificaciones y compensaciones: tramo, condición, valor y unidad |
| Cantidades con unidad dentro de las notas (plazos en días, mínimos en toneladas…) |
| Texto íntegro de las notas solo explicativas de cada PDF |
| Por PDF: cifras totales, guardadas, descartadas y sin cubrir |
| Registro de cada carga: altas, cambios, desapariciones y avisos |
Cada actualizar deja además su resumen (fecha y hora, versión, cifras e incidencias) en
raw/AAAA-MM-DD/ejecuciones.jsonl. Así reconstruir recupera el historial de ejecuciones
reales (origen = 'registro') y se anota a sí misma como un evento tipo = 'reconstruccion'.
Historial (SCD tipo 2)
Cada fila lleva primera_vez_visto y ultima_vez_visto (fechas de foto). Si el dato sigue igual
solo avanza ultima_vez_visto; si cambia, la fila antigua se queda y se inserta otra. Unos
triggers rechazan cualquier DELETE y cualquier UPDATE que no sea avanzar ultima_vez_visto.
Las vistas <tabla>_vigentes muestran lo visto en la última foto. fecha_precio es la vigencia
que publica la CNMC (puede faltar) y anio_tabla el año cuando la fuente solo da el año.
Cómo se evita comparar servicios distintos
tipo_precio:numerico,regla(«según contrato», «coste real + 3 %»…),textoopdf(remite a un documento). Si no es numérico,valores nulo y se guarda el texto original.tipo_valor:unico,maxomin, cada uno con su fecha.categoria_servicio: sale de config/categorias_servicio.yaml (etiqueta + tipo de instalación → categoría). No hay expresiones regulares que adivinen: lo que no está en el fichero queda comosin_categoriay genera un aviso.Indicadores
incluye_transporte,incluye_almacenamiento,incluye_expedicioneincluye_puesta_a_bordo, derivados de la lista «Incluye:» mediante config/indicadores_incluye.yaml.comparar_serviciosolo agrupa precios con la misma categoría, los mismos indicadores y la misma unidad, y avisa cuando hay más de un grupo.
Los PDF, leídos por completo
Muchos precios no están en la página: el detalle remite a un PDF. Cada tipo de PDF tiene su parser (elegido por el título del documento, no por su número) que trabaja sobre palabras con coordenadas, y cada valor guarda página, posición y texto original para poder auditarlo.
Sin punteros sueltos. La vista
lineas_pdf_sin_resolverlista las líneas contipo_precio = 'pdf'cuyo documento no tiene datos estructurados; debe estar vacía, y si no lo está la carga registra un error.Cobertura comprobada en cada carga. Todas las palabras con cifras de cada PDF deben estar en la base (como valor, dentro de una etiqueta o en una nota guardada) o en la lista de descartes justificados de config/descartes_pdf.yaml. Una cifra que no esté en ningún sitio es un error
cifra_pdf_sin_cubriry la ejecución termina en rojo. El resultado queda en la tablacobertura_pdf.Nada se inventa. Una cifra que no se puede interpretar con certeza (p. ej. «01021», sin coma) se guarda con valor nulo, su texto original y un aviso.
OCR solo de respaldo. Si una página no tiene capa de texto se lee con Tesseract (
uv sync --extra ocr); esos valores llevanmetodo = 'ocr'y generan un aviso para revisarlos. Nunca se pasa OCR a una página que ya tiene texto.Sin repeticiones. Una tarifa en PDF vale para todos los detalles que remiten a ella:
buscar_preciosycomparar_serviciola devuelven una sola vez, conaplica_a(nº de detalles e instalaciones, y la lista condetallar_aplicacion=true).Enlace por datos, no por nombres. Cada fila de las tablas origen-destino del PDF se une al detalle cuyos precios en la página web son exactamente los mismos; las matrices de reconsignación heredan esa identificación por nombre exacto.
Decisiones de diseño
Decisión | Motivo |
Crudo inmutable + base reconstruible | Un fallo de un parser se arregla sin perder ni volver a descargar nada |
Parsers guiados por etiquetas de texto («PRODUCTOS:», «Incluye:»…) | El texto visible cambia menos que las clases CSS |
Correspondencias en YAML revisable | Las decisiones de negocio se leen y se discuten sin leer código |
Operador = código del buscador | Los nombres cambian (Cepsa → Moeve); el código no |
Control cruzado fichas ↔ buscador y HTML ↔ PDF | Detecta descargas incompletas; ya cazó un fallo real de paginación |
Peticiones sin cookies | El buscador guarda la página en la sesión y contaminaba la búsqueda siguiente |
Decodificación cabecera → UTF-8 → ISO-8859-1 | El portal declara ISO-8859-1 en el |
| En origen-destino un cero no es un precio; se guarda |
SQLite en el repositorio | Clonar basta para consultar; el histórico viaja con el código |
Actualización automática y reconstrucción
CI en cada push:
ruffypytest(los tests usan páginas reales guardadas como fixtures y no tocan la red).Actualización mensual (día 2): ejecuta
descubriryactualizar, hace commit de la base y del inventario, y suberaw/AAAA-MM-DDcomo.tar.gza una Release. Si hubo errores, el trabajo queda en rojo después de conservar todo lo descargado.
Para reconstruir la base desde cero se descargan los crudos de las Releases:
gh release list --json tagName --jq '.[].tagName | select(startswith("raw-"))' |
while read -r etiqueta; do gh release download "$etiqueta" --pattern "$etiqueta.tar.gz" --dir /tmp/raw; done
mkdir -p raw && for f in /tmp/raw/*.tar.gz; do tar -xzf "$f" -C raw; done
uv run cnmc-logistica reconstruir --reemplazarLimitaciones conocidas
El histórico empieza el 11/11/2026; no hay datos anteriores.
La provincia no se publica de forma estructurada y no se guarda en esta versión.
El portal no fecha las rutas origen-destino: solo se conoce el año de la tabla (del PDF).
Los productos se separan por comas y por « y », sin fusionar variantes («Gasóleo marino» y «Gasóleos marinos» son dos valores).
Un PDF de un tipo nuevo no tiene parser: se registra el error
pdf_sin_parser, sus cifras quedan sin cubrir y hay que escribirle uno. Es deliberado: no se adivina su estructura.Los precios de los PDF solo tienen año (
anio_tabla), no fecha de vigencia.El PDF de búnker de Exolum no indica la unidad de sus precios; se guarda nula.
Hasta hoy ningún PDF ha necesitado OCR; el respaldo está probado con una página escaneada de prueba.
Desarrollo
uv sync
uv run ruff check . && uv run ruff format --check .
uv run pytestLicencia
Código bajo licencia MIT. Los datos son de la CNMC y de los operadores que los declaran.
Available Tools
12 toolsbonificacionesA
Bonificaciones y compensaciones de los contratos a largo plazo (PDF de Exolum).
Por cumplir (bonificación) o incumplir (compensación) las cantidades
garantizadas. Hay un documento por familia de productos; aplica_a dice a qué
productos e instalaciones corresponde. producto: filtra por parte del nombre
(«queroseno», «gasolinas»). Incluye los umbrales en % y las notas del documento.
Inputs: producto. Returns: object
buscar_preciosA
Precios vigentes con filtros. No agrupa: para comparar usa comparar_servicio.
Cada precio dice su fuente: «html» (la página del detalle) o «pdf» (una tarifa
en PDF a la que remite el detalle; documentos es el PDF y pagina_pdf la
página). Una línea «html» con tipo_precio «pdf» es solo el puntero al documento;
resuelto_en dice en qué tablas están sus datos. Un precio de PDF que vale
para varios detalles sale una sola vez, con aplica_a (nº de detalles e
instalaciones); con detallar_aplicacion=true incluye la lista.
categoria: categoria_servicio exacta (p. ej. «almacenamiento»). texto: parte de la etiqueta o subetiqueta del servicio. tipo_precio puede ser numerico, regla (p. ej. «según contrato»), texto o pdf (remite a un documento; valor nulo).
Inputs: texto, limite, operador, producto, +5 more. Returns: object
capacidadesB
Capacidad de almacenamiento (miles de m3) por compañía, instalación y producto.
Fuente: PDF «Características de las instalaciones de almacenamiento» de la CNMC. producto = «TOTAL» es el total de la instalación. id_tanque es nulo cuando la instalación del PDF no tiene ficha en el portal. ambito: «almacenamiento» o «aeroportuaria». Una cifra nula significa que el PDF lista el producto sin desglose.
Inputs: ambito, limite, compania, producto, +3 more. Returns: object
comparar_servicioB
Compara el precio de una categoría de servicio entre instalaciones y operadores.
Solo son comparables los precios de un mismo grupo: misma categoría, mismos
indicadores de lo que incluye el servicio (transporte, almacenamiento,
expedición, puesta a bordo) y misma unidad. Si salen varios grupos, la
respuesta lo avisa: no mezcles sus cifras. Incluye los precios de las tarifas
en PDF (fuente «pdf», con su documento y página; solo tienen año, no fecha).
Un precio de PDF común a varios detalles aparece una vez, con aplica_a
(detallar_aplicacion=true añade la lista de detalles).
Los servicios sin precio numérico se listan aparte con resuelto_en.
Inputs: categoria (required), operador, producto, comunidad, +2 more. Returns: object
consulta_sqlA
Ejecuta una única sentencia SELECT (o WITH … SELECT) sobre la base, en solo lectura.
Tablas principales: instalaciones, detalles, detalle_productos, operadores,
operadores_nombres, precios, condiciones, servicios_incluidos,
precios_origen_destino, notas_origen_destino, capacidades, documentos,
filas_documento, enlaces_documento, precios_pdf, matrices_pdf, bonificaciones,
notas_pdf, condiciones_pdf, cobertura_pdf, ejecuciones, incidencias. Vistas útiles:
precios_pdf_por_detalle, reconsignaciones, lineas_pdf_sin_resolver. Cada tabla
con historial tiene una vista _vigentes con lo visto en la última foto.
La función plano(texto) quita tildes y mayúsculas. Usa estado para ver las
columnas.
Inputs: sql (required), limite. Returns: object
estadoA
Estado de la base: última foto del portal, volumen de datos, incidencias y tablas.
Inputs: no named parameters. Returns: object
historial_precioB
Todas las versiones de los precios de un detalle, vigentes o no.
Cada fila dice entre qué fotos se vio (primera_vez_visto … ultima_vez_visto) y
la fecha de vigencia que publica la CNMC (fecha_precio). vigente indica si
sigue en la última foto. etiqueta/subetiqueta: parte del nombre del servicio.
Inputs: id_tanque_instalacion (required), etiqueta, subetiqueta. Returns: object
listar_instalacionesB
Instalaciones vigentes, con filtros opcionales.
operador: código (CLH, DECAL…) o parte del nombre. tipo: «almacenamiento» o «aeroportuaria». comunidad: código (AND, CAN…) o nombre. producto: parte del nombre («queroseno», «gasóleo»…); no distingue mayúsculas ni tildes.
Inputs: tipo, limite, operador, producto, +1 more. Returns: object
listar_operadoresB
Operadores (almacenistas) con su nº de instalaciones y detalles, por tipo.
Una instalación con dos titulares (p. ej. Exolum Corporation y Exolum Aviation
en un aeropuerto) cuenta para los dos. nombres_anteriores son los nombres
con que el portal ha publicado antes ese mismo código.
Inputs: solo_con_instalaciones. Returns: object
precios_origen_destinoB
Precio de llevar producto desde un origen hasta una instalación de destino.
origen: «Huelva» encuentra HUELVA y REFINERÍA HUELVA, que son orígenes distintos (cada fila dice cuál). destino: nombre de la instalación o del destino. El mismo par origen-destino tiene un precio por modalidad de recepción y por producto. Las rutas «no_ofrecida» no existen (no son gratis); por defecto se omiten.
Inputs: limite, origen, destino, producto, +2 more. Returns: object
reconsignacionesA
Precios de las matrices de los PDF de Exolum entre dos instalaciones.
tipo «reconsignacion» (por defecto): cambiar la instalación de entrega de
gasolinas y gasóleos, de la instalación ORIGEN a la de DESTINO. tipo
«consignacion_ester»: consignación del éster metílico para mezcla en línea.
tipo «origen_destino»: las tablas origen-destino tal como vienen en el PDF
(para el día a día usa precios_origen_destino, que parte de la página web).
origen/destino: parte del nombre («Huelva», «Rivabellosa»). Cada precio
indica el PDF, la página y el año de la tabla. Estado «no_aplica» = «(*)».
Inputs: tipo, limite, origen, destino. Returns: object
ver_instalacionA
Ficha completa de una instalación: detalles, precios, condiciones y capacidad.
De cada detalle se dan los precios de su página (precios), los de las tarifas
en PDF a las que remite (precios_pdf, con documento y página) y la lista de
esos PDF (documentos_pdf). Las matrices se consultan con reconsignaciones
y precios_origen_destino; las bonificaciones, con bonificaciones.
Inputs: id_tanque (required). Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 12 tools
Las herramientas de precios (buscar_precios, comparar_servicio, historial_precio, precios_origen_destino, reconsignaciones) podrían solaparse a primera vista, pero las descripciones delimitan bien cada alcance. ver_instalacion también incluye precios y capacidad, lo que puede crear alguna duda frente a buscar_precios/capacidades, pero no es grave.
Todos los nombres están en español y snake_case, sin mezclar camelCase. Hay mezcla de verbos en infinitivo (listar_, ver_, buscar_, comparar_) y sustantivos de recurso (estado, capacidades, bonificaciones), pero el patrón es legible y consistente por tipo de operación.
12 herramientas es un número adecuado para el dominio: cubre consulta, comparación, histórico, matrices y SQL sin exceder. Cada herramienta parece aportar una capacidad diferenciada dentro del alcance read-only del servidor.
Cubre el ciclo de consulta del dominio (instalaciones, operadores, precios, capacidades, matrices, bonificaciones) y ofrece SQL como vía de escape. Faltan herramientas dedicadas para documentos/PDF o búsquedas genéricas, pero son lagunas menores y workaround mediante consulta_sql.
Maintenance
Related MCP Connectors
Cheapest fuel in Spain: live prices at ~12,000 petrol stations, near a place or along a route.
Live official fuel station prices for France, Spain, Italy and Western Australia in one schema.
Your Nenpi car log: ask about costs, fuel use and reminders, add fill-ups. Free account at nenpi.app
US gas and diesel prices by station, ZIP or city. Paid, on Apify's hosted MCP.
Related MCP Servers
AlicenseAqualityBmaintenanceReal-time and historical oil, gas, and commodity prices. 40+ energy commodities including Brent Crude, WTI, Natural Gas, LBMA Gold/Silver, EU Carbon, and refined products. Get current prices, compare commodities, view market overviews, and access historical data — all through natural language. Used by energy traders, fintech companies, and researchers worldwide.32128 npm4MIT- AlicenseAqualityBmaintenanceProvides real-time fuel prices at gas stations in Spain using the government's public API. Enables users to find the cheapest fuel in a municipality or list stations sorted by price.2MIT
- AlicenseNot gradedqualityBmaintenanceEnables querying current and historical retail petrol, diesel, and LPG prices across European countries, comparing prices by week, and checking data coverage.141 npmMIT
- AlicenseNot gradedqualityDmaintenanceEnables read-only, natural-language queries of official Brazilian ANP installation records through any MCP-compatible client, with usage billed via prepaid credits.MIT