
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
pitwall-mcp
Advanced tools
Servidor MCP local, de solo lectura, sobre la API BMW CarData de un BMW X1 sDrive18i (U11, gasolina, España).
Proyecto no afiliado a BMW AG. "BMW" y "CarData" son marcas de sus respectivos propietarios. Este proyecto no está respaldado, patrocinado ni revisado por BMW.
Le da a un asistente acceso a los datos de mantenimiento del coche — kilometraje, avisos CBS, presiones de neumáticos, batería de 12 V — sin que pueda tocar nada del vehículo, y sin agotar la cuota diaria de la API.
Le preguntas al asistente "¿qué le toca al coche?" y contesta con esto. Es una respuesta real, de la lectura del 16 de septiembre de 2026, recortada:
RESUMEN DE MANTENIMIENTO
Kilometraje: 48.731 km
Proximo servicio en: 13460 km
OJO: hay partidas CBS que BMW ya no marca como OK; no te fies solo de la
cifra global: Frenos delanteros [PENDING]: 1.600 km.
Partidas CBS: 5
- Frenos delanteros [PENDING]: 1.600 km
- Aceite de motor [OK]: 14.000 km, hasta 2027-07
- Inspeccion tecnica (ITV) [OK]: hasta 2027-01
conditionBasedServicesCount dice 9: es el maximo de avisos que este vehiculo
puede transmitir, no los que tiene. El desglose trae 5, los transmitidos.
Prevision orientativa (calculo propio sobre los km de BMW, no un dato de BMW):
Ritmo usado: 570 km/semana (media semanal que da BMW); tu historico local,
8 dias: 420. Se usa el mayor para no quedarse corto.
- Frenos delanteros: 1.600 km, unas 2,8 semanas, hacia el 05-10-2026.
Bateria de 12V:
Voltaje: 13.92 V (2026-09-14 13:49 UTC (hace 40 horas))
Dato mas antiguo utilizado: 2026-09-14 13:49 UTC (hace 40 horas).
Procedencia: cache local. Lectura realizada: 2026-09-16 06:09 UTC.
Fíjate en lo que hace esa respuesta además de dar números: avisa de que la cifra global esconde lo urgente, traduce "1.600 km" a una fecha, admite que dos cifras de BMW no cuadran en vez de elegir una, y dice de cuándo es el dato más viejo que ha usado.
Sí:
No, y no lo hará:
get_software_version: ese descriptor no existe en el catálogo de
BMW. Lo único disponible es puStep, el paso de actualización de producto.La API REST de CarData está limitada a 50 peticiones cada 24 h y por cuenta.
Pasarse devuelve 403 CU-429 hasta el día siguiente, y el contador es de BMW:
incluye cualquier otra aplicación que use la misma cuenta.
Por eso:
search_descriptors y get_api_quota no gastan cuota nunca.Consulta get_api_quota antes de encadenar llamadas.
| Dato | TTL |
|---|---|
/telematicData (contenedor de mantenimiento) | 12 h |
/smartMaintenanceTyreDiagnosis | 7 días |
/basicData | 30 días |
/mappings | 30 días |
La app te enseña el estado de hoy. Esto guarda cada lectura, así que puede comparar. Cuatro cosas concretas que salen de ahí, todas vistas en este coche:
PENDING a
1.900 km, serviceDistance.next marcaba 13.560. Quedarse con ese número
habría escondido lo único urgente, así que toda partida que BMW no marque
OK sale con un aviso propio.Y lo que no aporta, para que no haya malentendidos:
-NA- son tres cosas distintas, y las herramientas las distinguen en vez de
enseñar un cero. De los 42 descriptores del contenedor, en la última lectura
llegaron 28 con valor y 14 vacíos.| Herramienta | Gasta cuota | Estado |
|---|---|---|
search_descriptors(query, limit, include_electric) | no | funciona |
get_api_quota() | no | funciona |
list_vehicles() | sí (1/mes con caché) | funciona |
get_vehicle_basic_data() | sí (1/mes con caché) | funciona |
report_product_update_step() | sí (comparte caché con basicData) | funciona, pero este vehículo no devuelve puStep |
get_telematic_data() | sí (2/día con caché) | funciona |
get_vehicle_status() | sí (comparte caché) | funciona |
get_tyre_diagnosis() | sí (1/semana con caché) | funciona, pero este vehículo devuelve la estructura vacía |
get_maintenance_summary() | sí (compuesta) | funciona |
diagnose_software_update() | sí (compuesta, comparte caché) | funciona, con veredicto acotado |
get_fuel_status() | sí (comparte caché) | funciona |
get_fault_memory() | sí (comparte caché) | funciona; no traduce los códigos |
Las dos herramientas marcadas con una reserva funcionan y declaran la
ausencia: no rellenan el hueco con ceros ni con un valor plausible. puStep
no llega en /basicData para este coche, y el diagnóstico de neumáticos vuelve
con etiquetas y ceros de relleno que no son medidas.
get_vehicle_status() y get_maintenance_summary() muestran también los avisos
Check Control, y añaden una línea OJO cuando alguna partida CBS deja de
estar en OK. La cifra global de próximo servicio no basta: con los frenos
delanteros en PENDING a 1.900 km, marcaba 13.560.
get_maintenance_summary() añade además, sin gastar peticiones, una
previsión en semanas y fechas y la tendencia de presiones por eje. Las dos salen
del histórico local, y el porqué de cada una está en
Por qué esto y no la app oficial. La
previsión usa la media semanal de BMW o la del histórico, la mayor de las dos,
para no quedarse corta, y avisa cuando una partida vence antes por kilómetros
que por fecha.
Dos herramientas más con datos que la app oficial no enseña:
get_fuel_status(): depósito y autonomía, los repostajes que detecta en
el histórico y el consumo real desde el último. Solo da el consumo a partir de
300 km, y siempre con su margen, porque el aforador puede desviarse hasta 6 L.
Muestra también el consumo homologado (OBFCM), pero como lo que es: una cifra
de por vida que en este coche no se mueve desde octubre de 2024, no el consumo
de hoy.get_fault_memory(): la memoria de averías que el coche guarda para el
taller, agrupada por centralita, y qué códigos aparecen o desaparecen entre
lecturas. No traduce ningún código: su significado no está en el catálogo
de BMW, y darlo sería inventarlo.diagnose_software_update() razona sobre la serie del histórico, no sobre
una foto. De las tres condiciones que BMW documenta para no ofrecer una
actualización, CarData sólo permite observar una, y la herramienta declara las
otras dos como NO OBSERVABLE POR CARDATA en lugar de razonar como si las
hubiera descartado. Deja fuera cualquier voltaje de 13,5 V o más, porque eso es
el alternador cargando y no dice nada de la batería en reposo. Si no le quedan al
menos dos observaciones en reposo, responde SIN VEREDICTO y dice qué le
falta. Y como isIgnitionOn llega vacío, avisa de que parte de cualquier
pendiente puede ser sólo motor en marcha frente a motor parado, no una batería
descargándose.
El histórico que va guardando el servidor no se ve en ningún sitio: las herramientas contestan con la foto del momento, no con la serie. Para mirar la serie hay una página HTML que se genera a mano desde la SQLite:
.venv/Scripts/python.exe scripts/report.py # captures/report-*.html
.venv/Scripts/python.exe scripts/report.py --full-vin # con el VIN entero
No gasta ninguna petición. No usa el token, no llama a ningún endpoint y no
toca el contador de cuota: sólo lee la base de datos local. La página es un
único fichero sin JavaScript ni dependencias externas, imprime bien, y sale con
el VIN enmascarado a captures/, que está en .gitignore.
Lo primero que enseña no es un número, es cuántos de los 42 descriptores del contenedor traen valor de verdad, y en cuál de los tres estados vacíos está cada uno de los demás.
Requiere Python 3.12+.
git clone https://github.com/sergioprats/pitwall-mcp
cd pitwall-mcp
python -m venv .venv
.venv/Scripts/python.exe -m pip install -e ".[dev]" # Linux/macOS: .venv/bin/python
.venv/Scripts/python.exe scripts/refresh_catalogue.py
Ese último paso no es opcional: el catálogo telemático es un documento de
BMW y este repositorio no lo redistribuye, así que hay que descargarlo una vez.
Viene de zweckj/bmw-cardata (MIT), no
de la API de CarData, de modo que no gasta ninguna petición de tu cuota. Los
detalles están en spec/README.md.
Si instalas el paquete en vez de clonar el repositorio no tienes scripts/, así
que el ejecutable trae lo imprescindible: --fetch-catalogue para el catálogo
y --login para autenticarte. Lo que sigue necesitando el repositorio es
crear el contenedor telemático, y es a propósito: ese paso escribe en tu
cuenta de BMW y por norma del proyecto no vive dentro del servidor.
pitwall-mcp --fetch-catalogue # junto a la base de datos
pitwall-mcp --fetch-catalogue --out /otra/ruta.json
Se niega a escribir si lo que baja no es un catálogo —un portal cautivo responde 200 con HTML—, así que un catálogo que ya te funcionaba no se pierde por una descarga mala.
Los comandos de abajo usan el intérprete del entorno virtual de forma explícita
(.venv/Scripts/python.exe en Windows, .venv/bin/python en Linux y macOS).
Si activas el entorno (.venv/Scripts/activate, o source .venv/bin/activate),
te basta con python. Sin activar y con el python del sistema, fallará con
ModuleNotFoundError: No module named 'bmw_cardata'.
cp .env.example .env
Rellena PITWALL_CLIENT_ID con el client id de tu aplicación CarData, creada en
el portal BMW CarData. Necesitas los scopes
cardata:api:read y, si algún día usas streaming, cardata:streaming:read.
pitwall-mcp --login # instalado con pip
.venv/Scripts/python.exe scripts/login.py # o desde el repositorio: hace lo mismo
Device flow: imprime una URL y un código, tú lo autorizas en el navegador. Los
tokens se guardan en ~/.config/pitwall-mcp/tokens.json con permisos 600,
fuera del repositorio. Habla con el OAuth de BMW, no con la API de CarData,
así que no gasta cuota.
El script del repositorio es solo un envoltorio del mismo código: una única
definición del flujo, y así quien instala con pip también puede autenticarse.
El refresh token dura 14 días. El servidor avisa de forma visible cuando quedan menos de 3; si caduca, hay que repetir este paso a mano.
.venv/Scripts/python.exe scripts/bootstrap_containers.py --dry-run # muestra qué enviaría
.venv/Scripts/python.exe scripts/bootstrap_containers.py --create # gasta 1 petición
Copia el containerId resultante a PITWALL_CONTAINER_ID en el .env. El
servidor MCP solo consume ese id: nunca crea ni borra nada.
El contenedor pide 42 descriptores. Además del mantenimiento, incluye el depósito, la autonomía, el consumo homologado, la memoria de averías y la temperatura del motor. En el coche de referencia llegan todos menos tres. Si tu contenedor es de antes de esta ampliación, créalo de nuevo con el mismo script.
.venv/Scripts/python.exe -m pitwall_mcp
Transporte stdio. El .env se busca en el directorio desde el que se lanza
el servidor y, si no aparece, en la raíz del repositorio — un cliente MCP arranca
el proceso con el directorio de trabajo que le apetece. Si tu .env vive en otro
sitio, indícalo con PITWALL_ENV_FILE.
Para Claude Desktop o cualquier cliente MCP:
{
"mcpServers": {
"pitwall": {
"command": "/ruta/al/.venv/bin/python",
"args": ["-m", "pitwall_mcp"],
"env": { "PITWALL_CLIENT_ID": "...", "PITWALL_VIN": "...", "PITWALL_CONTAINER_ID": "..." }
}
}
}
.venv/Scripts/python.exe -m pytest # ningún test toca la red
.venv/Scripts/python.exe scripts/refresh_catalogue.py --check # ¿ha cambiado el catálogo?
Ningún test hace llamadas reales. Todos van contra fixtures grabados a mano
a partir de los esquemas del swagger. Una suite que gaste cuota es un bug, y la
fixture no_network lo hace cumplir: hace fallar cualquier intento de abrir una
conexión de verdad.
Eso mismo corre en GitHub Actions en cada push y en cada pull request, sobre Linux y Windows, con Python 3.12 y 3.13. El flujo empieza descargando el catálogo, porque el repositorio no lo redistribuye, y no necesita ningún secreto: si un test intentara llamar a la API de BMW, fallaría.
Además se ejecuta una vez por semana, sin que nadie toque nada. No es por costumbre: como el catálogo se descarga en cada ejecución, esa pasada semanal avisa si el catálogo de origen deja de traer un descriptor en el que el proyecto se apoya. Entonces falla un test, y te enteras por correo en vez de por una herramienta comportándose raro meses después. Ojo con lo que eso no hace: no vigila cualquier cambio del catálogo, solo los que romperían algo.
Documentación relevante:
CLAUDE.md — documento de gobierno del repositorio.docs/resumen-del-proyecto.md — qué hace, cómo
lo hace y para qué sirve, para quien llega de nuevas.docs/paso-0-descriptores.md — los descriptores
confirmados, lo que no existe, y las rarezas del catálogo y del swagger.docs/publicacion.md — cómo se publica esto: el orden
de los pasos, y qué no debe salir del repositorio.docs/streaming-design.md — la Fase 2, el daemon
MQTT. Diseño, no código.Todas las herramientas están implementadas contra respuestas reales grabadas
como fixtures. La Fase 2 (daemon MQTT de streaming) es diseño, no código; el
esquema SQLite ya reserva la columna source y la tabla stream_state para no
necesitar migración.
Lo que ya se sabe de este coche, verificado con lecturas reales entre el 7 y el 18 de septiembre de 2026:
diagnose_software_update
se queda en SIN VEREDICTO en vez de fingir uno. La única vía conocida para
esa serie es el streaming de la Fase 2.MIT, © 2026 Panesoft. Ver LICENSE.
Los documentos de BMW no se redistribuyen desde aquí. Ni el catálogo
telemático ni la especificación OpenAPI están en el control de versiones: se
descargan en tu máquina, y spec/ está en .gitignore. Ver
spec/README.md.
Proyecto no afiliado a BMW AG. "BMW" y "CarData" son marcas de sus respectivos propietarios.
FAQs
Read-only MCP server over the BMW CarData customer API. Not affiliated with BMW AG.
We found that pitwall-mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.