balizamcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +371 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 granero
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# balizamcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/balizamcp)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://modelcontextprotocol.io)
|
|
6
|
+
[](https://balizame.com)
|
|
7
|
+
|
|
8
|
+
**Servidor MCP oficial de [balizame.com](https://balizame.com) para datos en tiempo real de balizas V16 activas en Espana.**
|
|
9
|
+
|
|
10
|
+
Conecta Claude y otros LLMs con el [Punto de Acceso Nacional (NAP)](https://nap.dgt.es) de la DGT, obteniendo las ubicaciones de vehiculos detenidos/averiados que estan transmitiendo su posicion mediante balizas V16 conectadas
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Caracteristicas
|
|
15
|
+
|
|
16
|
+
- **Datos en tiempo real**: Actualizacion cada minuto desde el NAP de la DGT
|
|
17
|
+
- **Filtrado inteligente**: Solo muestra incidentes tipo `vehicleObstruction` (balizas V16 activas)
|
|
18
|
+
- **Cobertura nacional**: Todas las balizas V16 conectadas en Espana
|
|
19
|
+
- **Informacion completa**: Coordenadas GPS, carretera, km, municipio, provincia, severidad
|
|
20
|
+
- **Cache inteligente**: Minimiza llamadas al servidor respetando la frecuencia de actualizacion
|
|
21
|
+
|
|
22
|
+
## Instalacion
|
|
23
|
+
|
|
24
|
+
### Opcion 1: npx (recomendado)
|
|
25
|
+
|
|
26
|
+
No necesitas instalar nada, usalo directamente:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx balizamcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Opcion 2: Instalacion global
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g balizamcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Opcion 3: Desde codigo fuente
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/granero/balizamcp.git
|
|
42
|
+
cd balizamcp
|
|
43
|
+
npm install
|
|
44
|
+
npm run build
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuracion en Claude Desktop
|
|
48
|
+
|
|
49
|
+
Anade a tu archivo de configuracion `claude_desktop_config.json`:
|
|
50
|
+
|
|
51
|
+
### macOS
|
|
52
|
+
```
|
|
53
|
+
~/Library/Application Support/Claude/claude_desktop_config.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Windows
|
|
57
|
+
```
|
|
58
|
+
%APPDATA%\Claude\claude_desktop_config.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Configuracion
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"balizas-v16": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "balizamcp"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
O si lo instalaste globalmente:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"balizas-v16": {
|
|
80
|
+
"command": "balizamcp"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Reinicia Claude Desktop** despues de modificar la configuracion.
|
|
87
|
+
|
|
88
|
+
## Herramientas disponibles
|
|
89
|
+
|
|
90
|
+
### `obtener_balizas_activas`
|
|
91
|
+
|
|
92
|
+
Obtiene las balizas V16 actualmente activas en Espana.
|
|
93
|
+
|
|
94
|
+
**Parametros** (todos opcionales):
|
|
95
|
+
|
|
96
|
+
| Parametro | Tipo | Descripcion |
|
|
97
|
+
|-----------|------|-------------|
|
|
98
|
+
| `provincia` | string | Filtrar por provincia (ej: "Madrid", "Barcelona") |
|
|
99
|
+
| `carretera` | string | Filtrar por carretera (ej: "A-1", "AP-7") |
|
|
100
|
+
| `severidad` | string | Filtrar por severidad: "baja", "media", "alta", "critica" |
|
|
101
|
+
| `limite` | number | Numero maximo de resultados |
|
|
102
|
+
|
|
103
|
+
**Ejemplo de uso en Claude:**
|
|
104
|
+
|
|
105
|
+
> "Cuantas balizas V16 hay activas en Madrid ahora mismo?"
|
|
106
|
+
|
|
107
|
+
**Respuesta:**
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"fuente": "DGT - Punto de Acceso Nacional (NAP) - DATEX2",
|
|
112
|
+
"actualizadoEn": "2026-01-16T17:00:00.000Z",
|
|
113
|
+
"totalEncontradas": 33,
|
|
114
|
+
"totalEspana": 322,
|
|
115
|
+
"balizas": [
|
|
116
|
+
{
|
|
117
|
+
"id": "dgt-8355636",
|
|
118
|
+
"coordenadas": { "lat": 40.416775, "lon": -3.703790 },
|
|
119
|
+
"ubicacion": {
|
|
120
|
+
"nombreCarretera": "A-6",
|
|
121
|
+
"km": 15.2,
|
|
122
|
+
"municipio": "Las Rozas",
|
|
123
|
+
"provincia": "Madrid",
|
|
124
|
+
"comunidadAutonoma": "Madrid"
|
|
125
|
+
},
|
|
126
|
+
"incidente": {
|
|
127
|
+
"tipo": "vehiculo_detenido",
|
|
128
|
+
"severidad": "baja",
|
|
129
|
+
"horaInicio": "2026-01-16T16:45:00.000+01:00"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `estadisticas_balizas`
|
|
137
|
+
|
|
138
|
+
Obtiene estadisticas agregadas de las balizas V16 activas.
|
|
139
|
+
|
|
140
|
+
**Ejemplo de uso en Claude:**
|
|
141
|
+
|
|
142
|
+
> "Dame estadisticas de las balizas V16 activas en Espana"
|
|
143
|
+
|
|
144
|
+
**Respuesta:**
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"estadisticas": {
|
|
149
|
+
"totalBalizasActivas": 322,
|
|
150
|
+
"porProvincia": {
|
|
151
|
+
"Madrid": 33,
|
|
152
|
+
"Valencia": 22,
|
|
153
|
+
"Alicante": 22,
|
|
154
|
+
"A Coruna": 20
|
|
155
|
+
},
|
|
156
|
+
"porSeveridad": {
|
|
157
|
+
"baja": 317,
|
|
158
|
+
"critica": 3,
|
|
159
|
+
"alta": 1,
|
|
160
|
+
"media": 1
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Recurso MCP
|
|
167
|
+
|
|
168
|
+
El servidor tambien expone un recurso MCP:
|
|
169
|
+
|
|
170
|
+
- **URI**: `balizas://activas`
|
|
171
|
+
- **Descripcion**: Listado completo de balizas V16 activas en formato JSON
|
|
172
|
+
|
|
173
|
+
## Que son las balizas V16?
|
|
174
|
+
|
|
175
|
+
Las **balizas V16** son dispositivos de senalizacion luminosa de emergencia que sustituyen a los triangulos de emergencia en Espana. Las balizas V16 **conectadas** (IoT) transmiten automaticamente su posicion GPS a la plataforma DGT 3.0 cuando se activan, permitiendo:
|
|
176
|
+
|
|
177
|
+
- Alertar a otros conductores a traves de sistemas de navegacion
|
|
178
|
+
- Mejorar la respuesta de servicios de emergencia
|
|
179
|
+
- Reducir el riesgo de atropellos en carretera
|
|
180
|
+
|
|
181
|
+
> Mas informacion: [balizame.com](https://balizame.com)
|
|
182
|
+
|
|
183
|
+
## Fuente de datos
|
|
184
|
+
|
|
185
|
+
Los datos provienen del **Punto de Acceso Nacional (NAP)** de la DGT en formato DATEX2:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
https://nap.dgt.es/datex2/v3/dgt/SituationPublication/datex2_v36.xml
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Este MCP filtra especificamente los incidentes de tipo `vehicleObstruction` y `obstruction`, que corresponden a vehiculos detenidos/averiados con baliza V16 activa.
|
|
192
|
+
|
|
193
|
+
## Desarrollo
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Clonar repositorio
|
|
197
|
+
git clone https://github.com/granero/balizamcp.git
|
|
198
|
+
cd balizamcp
|
|
199
|
+
|
|
200
|
+
# Instalar dependencias
|
|
201
|
+
npm install
|
|
202
|
+
|
|
203
|
+
# Desarrollo con hot-reload
|
|
204
|
+
npm run dev
|
|
205
|
+
|
|
206
|
+
# Compilar
|
|
207
|
+
npm run build
|
|
208
|
+
|
|
209
|
+
# Probar con MCP Inspector
|
|
210
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Licencia
|
|
214
|
+
|
|
215
|
+
[MIT](LICENSE) - Libre para uso personal y comercial.
|
|
216
|
+
|
|
217
|
+
## Creditos
|
|
218
|
+
|
|
219
|
+
- Datos: [DGT - Punto de Acceso Nacional](https://nap.dgt.es)
|
|
220
|
+
- Inspiracion: [balizame.com](https://balizame.com)
|
|
221
|
+
- Protocolo: [Model Context Protocol](https://modelcontextprotocol.io)
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
**Problemas o sugerencias?** [Abre un issue](https://github.com/granero/balizamcp/issues)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* balizamcp - Datos en tiempo real de balizas V16 activas en España
|
|
4
|
+
*
|
|
5
|
+
* Servidor MCP oficial de balizame.com
|
|
6
|
+
* Obtiene información del Punto de Acceso Nacional (NAP) de la DGT
|
|
7
|
+
* filtrando incidentes de tipo vehicleObstruction (vehículos detenidos/averiados).
|
|
8
|
+
*
|
|
9
|
+
* @author granero
|
|
10
|
+
* @license MIT
|
|
11
|
+
* @see https://balizame.com
|
|
12
|
+
* @see https://github.com/granero/balizamcp
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* balizamcp - Datos en tiempo real de balizas V16 activas en España
|
|
4
|
+
*
|
|
5
|
+
* Servidor MCP oficial de balizame.com
|
|
6
|
+
* Obtiene información del Punto de Acceso Nacional (NAP) de la DGT
|
|
7
|
+
* filtrando incidentes de tipo vehicleObstruction (vehículos detenidos/averiados).
|
|
8
|
+
*
|
|
9
|
+
* @author granero
|
|
10
|
+
* @license MIT
|
|
11
|
+
* @see https://balizame.com
|
|
12
|
+
* @see https://github.com/granero/balizamcp
|
|
13
|
+
*/
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
// ============= CONFIGURACIÓN =============
|
|
18
|
+
const DGT_NAP_URL = "https://nap.dgt.es/datex2/v3/dgt/SituationPublication/datex2_v36.xml";
|
|
19
|
+
const CACHE_TTL_MS = 60_000; // 1 minuto (frecuencia de actualización del NAP)
|
|
20
|
+
const VERSION = "1.0.0";
|
|
21
|
+
/**
|
|
22
|
+
* Causas DATEX2 que indican vehículo detenido/avería.
|
|
23
|
+
* Estos son los casos donde típicamente se activa una baliza V16.
|
|
24
|
+
* @see https://balizame.com para más información sobre balizas V16
|
|
25
|
+
*/
|
|
26
|
+
const VEHICLE_OBSTRUCTION_CAUSES = new Set(["vehicleObstruction", "obstruction"]);
|
|
27
|
+
// ============= PARSING DATEX2 =============
|
|
28
|
+
function extractCoordinates(xml) {
|
|
29
|
+
// Buscar latitud y longitud
|
|
30
|
+
const latMatch = xml.match(/<[^:]*:?latitude[^>]*>([^<]+)</i);
|
|
31
|
+
const lonMatch = xml.match(/<[^:]*:?longitude[^>]*>([^<]+)</i);
|
|
32
|
+
if (latMatch && lonMatch) {
|
|
33
|
+
const lat = parseFloat(latMatch[1].trim());
|
|
34
|
+
const lon = parseFloat(lonMatch[1].trim());
|
|
35
|
+
if (!isNaN(lat) && !isNaN(lon)) {
|
|
36
|
+
return { lat, lon };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function mapSeverity(severity) {
|
|
42
|
+
if (!severity)
|
|
43
|
+
return "baja";
|
|
44
|
+
const s = severity.toLowerCase();
|
|
45
|
+
if (s === "highest" || s === "critical")
|
|
46
|
+
return "crítica";
|
|
47
|
+
if (s === "high" || s === "severe")
|
|
48
|
+
return "alta";
|
|
49
|
+
if (s === "medium" || s === "moderate")
|
|
50
|
+
return "media";
|
|
51
|
+
return "baja";
|
|
52
|
+
}
|
|
53
|
+
function parseSituation(situationXml) {
|
|
54
|
+
// Extraer ID
|
|
55
|
+
const idMatch = situationXml.match(/id="([^"]+)"/);
|
|
56
|
+
if (!idMatch)
|
|
57
|
+
return null;
|
|
58
|
+
const id = idMatch[1];
|
|
59
|
+
// Extraer causa
|
|
60
|
+
const causeMatch = situationXml.match(/<[^:]*:?causeType[^>]*>([^<]+)</i);
|
|
61
|
+
const causa = causeMatch ? causeMatch[1].trim() : null;
|
|
62
|
+
// Solo procesar obstrucciones de vehículos (balizas V16)
|
|
63
|
+
if (!causa || !VEHICLE_OBSTRUCTION_CAUSES.has(causa)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// Extraer coordenadas
|
|
67
|
+
const coords = extractCoordinates(situationXml);
|
|
68
|
+
if (!coords)
|
|
69
|
+
return null;
|
|
70
|
+
// Extraer datos adicionales
|
|
71
|
+
const severityMatch = situationXml.match(/<[^:]*:?overallSeverity[^>]*>([^<]+)</i);
|
|
72
|
+
const roadMatch = situationXml.match(/<[^:]*:?roadNumber[^>]*>([^<]+)</i);
|
|
73
|
+
const roadNameMatch = situationXml.match(/<[^:]*:?roadName[^>]*>([^<]+)</i);
|
|
74
|
+
const kmMatch = situationXml.match(/<[^:]*:?kilometerPoint[^>]*>([^<]+)</i);
|
|
75
|
+
const directionMatch = situationXml.match(/<[^:]*:?tpegDirectionRoad[^>]*>([^<]+)</i);
|
|
76
|
+
const municipalityMatch = situationXml.match(/<[^:]*:?municipality[^>]*>([^<]+)</i);
|
|
77
|
+
const provinceMatch = situationXml.match(/<[^:]*:?province[^>]*>([^<]+)</i);
|
|
78
|
+
const autonomousCommunityMatch = situationXml.match(/<[^:]*:?autonomousCommunity[^>]*>([^<]+)</i);
|
|
79
|
+
const startTimeMatch = situationXml.match(/<[^:]*:?overallStartTime[^>]*>([^<]+)</i);
|
|
80
|
+
const lanesAffectedMatch = situationXml.match(/<[^:]*:?numberOfLanesRestricted[^>]*>([^<]+)</i);
|
|
81
|
+
const lanesTotalMatch = situationXml.match(/<[^:]*:?numberOfLanes[^>]*>([^<]+)</i);
|
|
82
|
+
const delayMatch = situationXml.match(/<[^:]*:?delayTimeValue[^>]*>([^<]+)</i);
|
|
83
|
+
const queueMatch = situationXml.match(/<[^:]*:?queueLengthValue[^>]*>([^<]+)</i);
|
|
84
|
+
const subtypeMatch = situationXml.match(/<[^:]*:?detailedCauseType[^>]*>[\s\S]*?<[^:>]+>([^<]+)</i);
|
|
85
|
+
// Extraer descripción del comentario
|
|
86
|
+
let descripcion;
|
|
87
|
+
const commentMatch = situationXml.match(/<[^:]*:?value[^>]*lang="es"[^>]*>([^<]+)</i) ||
|
|
88
|
+
situationXml.match(/<[^:]*:?value[^>]*>([^<]+)</i);
|
|
89
|
+
if (commentMatch) {
|
|
90
|
+
descripcion = commentMatch[1].trim().substring(0, 500);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
id: `dgt-${id}`,
|
|
94
|
+
lat: coords.lat,
|
|
95
|
+
lon: coords.lon,
|
|
96
|
+
carretera: roadMatch ? roadMatch[1].trim() : "",
|
|
97
|
+
nombreCarretera: roadNameMatch ? roadNameMatch[1].trim() : undefined,
|
|
98
|
+
kmInicio: kmMatch ? parseFloat(kmMatch[1]) : undefined,
|
|
99
|
+
direccion: directionMatch ? directionMatch[1].trim() : undefined,
|
|
100
|
+
municipio: municipalityMatch ? municipalityMatch[1].trim() : "",
|
|
101
|
+
provincia: provinceMatch ? provinceMatch[1].trim() : "",
|
|
102
|
+
comunidadAutonoma: autonomousCommunityMatch ? autonomousCommunityMatch[1].trim() : "",
|
|
103
|
+
descripcion,
|
|
104
|
+
severidad: mapSeverity(severityMatch ? severityMatch[1] : null),
|
|
105
|
+
tipoIncidente: "vehiculo_detenido",
|
|
106
|
+
subtipoIncidente: subtypeMatch ? subtypeMatch[1].trim() : undefined,
|
|
107
|
+
causa: causa,
|
|
108
|
+
horaInicio: startTimeMatch ? startTimeMatch[1].trim() : undefined,
|
|
109
|
+
carrilesAfectados: lanesAffectedMatch ? parseInt(lanesAffectedMatch[1]) : undefined,
|
|
110
|
+
carrilesTotales: lanesTotalMatch ? parseInt(lanesTotalMatch[1]) : undefined,
|
|
111
|
+
retrasoSegundos: delayMatch ? parseInt(delayMatch[1]) : undefined,
|
|
112
|
+
longitudColaMtrs: queueMatch ? parseInt(queueMatch[1]) : undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function parseDatex2(xml) {
|
|
116
|
+
const balizas = [];
|
|
117
|
+
// Buscar todas las situaciones
|
|
118
|
+
const situationRegex = /<[^:]*:?situation\s+[^>]*id="[^"]+">[\s\S]*?<\/[^:]*:?situation>/gi;
|
|
119
|
+
let match;
|
|
120
|
+
while ((match = situationRegex.exec(xml)) !== null) {
|
|
121
|
+
const baliza = parseSituation(match[0]);
|
|
122
|
+
if (baliza) {
|
|
123
|
+
balizas.push(baliza);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return balizas;
|
|
127
|
+
}
|
|
128
|
+
// ============= CACHE Y DATOS =============
|
|
129
|
+
let cachedData = null;
|
|
130
|
+
let cacheTimestamp = 0;
|
|
131
|
+
async function obtenerBalizasActivas() {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
// Usar caché si es válida (el NAP actualiza cada minuto)
|
|
134
|
+
if (cachedData && (now - cacheTimestamp) < CACHE_TTL_MS) {
|
|
135
|
+
return cachedData;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(DGT_NAP_URL, {
|
|
139
|
+
headers: {
|
|
140
|
+
"User-Agent": `balizamcp/${VERSION} (+https://balizame.com)`,
|
|
141
|
+
"Accept": "application/xml, text/xml, */*",
|
|
142
|
+
"Accept-Encoding": "gzip, deflate", // Solicitar compresión
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
const xml = await response.text();
|
|
149
|
+
const balizas = parseDatex2(xml);
|
|
150
|
+
cachedData = {
|
|
151
|
+
timestamp: now,
|
|
152
|
+
total: balizas.length,
|
|
153
|
+
balizasActivas: balizas,
|
|
154
|
+
fuente: "DGT - Punto de Acceso Nacional (NAP) - DATEX2",
|
|
155
|
+
ultimaActualizacion: new Date().toISOString(),
|
|
156
|
+
};
|
|
157
|
+
cacheTimestamp = now;
|
|
158
|
+
return cachedData;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const errorMsg = error instanceof Error ? error.message : "Error desconocido";
|
|
162
|
+
console.error(`[MCP Balizas V16] Error obteniendo datos del NAP: ${errorMsg}`);
|
|
163
|
+
// Devolver caché antigua si existe, o datos vacíos
|
|
164
|
+
if (cachedData) {
|
|
165
|
+
return {
|
|
166
|
+
...cachedData,
|
|
167
|
+
fuente: `DGT - NAP (caché: ${errorMsg})`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
timestamp: now,
|
|
172
|
+
total: 0,
|
|
173
|
+
balizasActivas: [],
|
|
174
|
+
fuente: `DGT - NAP (error: ${errorMsg})`,
|
|
175
|
+
ultimaActualizacion: new Date().toISOString(),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ============= SERVIDOR MCP =============
|
|
180
|
+
const server = new Server({
|
|
181
|
+
name: "balizamcp",
|
|
182
|
+
version: VERSION,
|
|
183
|
+
}, {
|
|
184
|
+
capabilities: {
|
|
185
|
+
tools: {},
|
|
186
|
+
resources: {},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
190
|
+
return {
|
|
191
|
+
tools: [
|
|
192
|
+
{
|
|
193
|
+
name: "obtener_balizas_activas",
|
|
194
|
+
description: "Obtiene en TIEMPO REAL las balizas V16 actualmente activas en España. Devuelve las ubicaciones de vehículos detenidos/averiados que han activado su baliza V16 y están comunicando su posición a la DGT 3.0. Incluye coordenadas GPS, carretera, km, municipio, provincia y tiempo de activación.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
provincia: {
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "Filtrar por provincia (ej: 'Madrid', 'Barcelona')",
|
|
201
|
+
},
|
|
202
|
+
carretera: {
|
|
203
|
+
type: "string",
|
|
204
|
+
description: "Filtrar por carretera (ej: 'A-1', 'AP-7')",
|
|
205
|
+
},
|
|
206
|
+
severidad: {
|
|
207
|
+
type: "string",
|
|
208
|
+
enum: ["baja", "media", "alta", "crítica"],
|
|
209
|
+
description: "Filtrar por severidad del incidente",
|
|
210
|
+
},
|
|
211
|
+
limite: {
|
|
212
|
+
type: "number",
|
|
213
|
+
description: "Número máximo de resultados",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "estadisticas_balizas",
|
|
220
|
+
description: "Obtiene estadísticas de las balizas V16 activas: total por provincia, por carretera y distribución por severidad.",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
230
|
+
const { name, arguments: args } = request.params;
|
|
231
|
+
if (name === "obtener_balizas_activas") {
|
|
232
|
+
const datos = await obtenerBalizasActivas();
|
|
233
|
+
let balizas = [...datos.balizasActivas];
|
|
234
|
+
// Aplicar filtros
|
|
235
|
+
if (args?.provincia) {
|
|
236
|
+
const busqueda = args.provincia.toLowerCase();
|
|
237
|
+
balizas = balizas.filter(b => b.provincia.toLowerCase().includes(busqueda));
|
|
238
|
+
}
|
|
239
|
+
if (args?.carretera) {
|
|
240
|
+
const busqueda = args.carretera.toLowerCase();
|
|
241
|
+
balizas = balizas.filter(b => b.carretera.toLowerCase().includes(busqueda));
|
|
242
|
+
}
|
|
243
|
+
if (args?.severidad) {
|
|
244
|
+
balizas = balizas.filter(b => b.severidad === args.severidad);
|
|
245
|
+
}
|
|
246
|
+
if (args?.limite && typeof args.limite === "number") {
|
|
247
|
+
balizas = balizas.slice(0, args.limite);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: JSON.stringify({
|
|
254
|
+
fuente: datos.fuente,
|
|
255
|
+
actualizadoEn: datos.ultimaActualizacion,
|
|
256
|
+
nota: "Datos en tiempo real de vehículos detenidos/averiados con baliza V16 activa comunicando a DGT 3.0",
|
|
257
|
+
totalEncontradas: balizas.length,
|
|
258
|
+
totalEspana: datos.total,
|
|
259
|
+
balizas: balizas.map(b => ({
|
|
260
|
+
id: b.id,
|
|
261
|
+
coordenadas: { lat: b.lat, lon: b.lon },
|
|
262
|
+
ubicacion: {
|
|
263
|
+
carretera: b.carretera,
|
|
264
|
+
nombreCarretera: b.nombreCarretera,
|
|
265
|
+
km: b.kmInicio,
|
|
266
|
+
direccion: b.direccion,
|
|
267
|
+
municipio: b.municipio,
|
|
268
|
+
provincia: b.provincia,
|
|
269
|
+
comunidadAutonoma: b.comunidadAutonoma,
|
|
270
|
+
},
|
|
271
|
+
incidente: {
|
|
272
|
+
tipo: b.tipoIncidente,
|
|
273
|
+
subtipo: b.subtipoIncidente,
|
|
274
|
+
severidad: b.severidad,
|
|
275
|
+
descripcion: b.descripcion,
|
|
276
|
+
horaInicio: b.horaInicio,
|
|
277
|
+
},
|
|
278
|
+
trafico: {
|
|
279
|
+
carrilesAfectados: b.carrilesAfectados,
|
|
280
|
+
carrilesTotales: b.carrilesTotales,
|
|
281
|
+
retrasoSegundos: b.retrasoSegundos,
|
|
282
|
+
longitudColaMtrs: b.longitudColaMtrs,
|
|
283
|
+
},
|
|
284
|
+
})),
|
|
285
|
+
}, null, 2),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (name === "estadisticas_balizas") {
|
|
291
|
+
const datos = await obtenerBalizasActivas();
|
|
292
|
+
// Contar por provincia
|
|
293
|
+
const porProvincia = {};
|
|
294
|
+
const porCarretera = {};
|
|
295
|
+
const porSeveridad = {};
|
|
296
|
+
for (const b of datos.balizasActivas) {
|
|
297
|
+
if (b.provincia) {
|
|
298
|
+
porProvincia[b.provincia] = (porProvincia[b.provincia] || 0) + 1;
|
|
299
|
+
}
|
|
300
|
+
if (b.carretera) {
|
|
301
|
+
porCarretera[b.carretera] = (porCarretera[b.carretera] || 0) + 1;
|
|
302
|
+
}
|
|
303
|
+
porSeveridad[b.severidad] = (porSeveridad[b.severidad] || 0) + 1;
|
|
304
|
+
}
|
|
305
|
+
// Ordenar por cantidad descendente
|
|
306
|
+
const ordenarDesc = (obj) => Object.entries(obj)
|
|
307
|
+
.sort((a, b) => b[1] - a[1])
|
|
308
|
+
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {});
|
|
309
|
+
return {
|
|
310
|
+
content: [
|
|
311
|
+
{
|
|
312
|
+
type: "text",
|
|
313
|
+
text: JSON.stringify({
|
|
314
|
+
fuente: datos.fuente,
|
|
315
|
+
actualizadoEn: datos.ultimaActualizacion,
|
|
316
|
+
estadisticas: {
|
|
317
|
+
totalBalizasActivas: datos.total,
|
|
318
|
+
porProvincia: ordenarDesc(porProvincia),
|
|
319
|
+
porCarretera: ordenarDesc(porCarretera),
|
|
320
|
+
porSeveridad: ordenarDesc(porSeveridad),
|
|
321
|
+
},
|
|
322
|
+
}, null, 2),
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
content: [{ type: "text", text: `Herramienta desconocida: ${name}` }],
|
|
329
|
+
isError: true,
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
333
|
+
return {
|
|
334
|
+
resources: [
|
|
335
|
+
{
|
|
336
|
+
uri: "balizas://activas",
|
|
337
|
+
name: "Balizas V16 activas en tiempo real",
|
|
338
|
+
description: "Listado de vehículos detenidos/averiados con baliza V16 activa comunicando a DGT 3.0",
|
|
339
|
+
mimeType: "application/json",
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
345
|
+
const { uri } = request.params;
|
|
346
|
+
if (uri === "balizas://activas") {
|
|
347
|
+
const datos = await obtenerBalizasActivas();
|
|
348
|
+
return {
|
|
349
|
+
contents: [
|
|
350
|
+
{
|
|
351
|
+
uri,
|
|
352
|
+
mimeType: "application/json",
|
|
353
|
+
text: JSON.stringify({
|
|
354
|
+
fuente: datos.fuente,
|
|
355
|
+
actualizadoEn: datos.ultimaActualizacion,
|
|
356
|
+
total: datos.total,
|
|
357
|
+
balizas: datos.balizasActivas,
|
|
358
|
+
}, null, 2),
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
throw new Error(`Recurso no encontrado: ${uri}`);
|
|
364
|
+
});
|
|
365
|
+
async function main() {
|
|
366
|
+
const transport = new StdioServerTransport();
|
|
367
|
+
await server.connect(transport);
|
|
368
|
+
console.error(`balizamcp v${VERSION} - https://balizame.com`);
|
|
369
|
+
console.error("Datos en tiempo real de balizas V16 desde DGT NAP");
|
|
370
|
+
}
|
|
371
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "balizamcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server oficial de balizame.com - Datos en tiempo real de balizas V16 activas en Espana via DGT NAP",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"balizamcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"balizas",
|
|
21
|
+
"v16",
|
|
22
|
+
"dgt",
|
|
23
|
+
"trafico",
|
|
24
|
+
"tiempo-real",
|
|
25
|
+
"espana",
|
|
26
|
+
"seguridad-vial",
|
|
27
|
+
"datex2",
|
|
28
|
+
"nap",
|
|
29
|
+
"claude",
|
|
30
|
+
"ai",
|
|
31
|
+
"balizame"
|
|
32
|
+
],
|
|
33
|
+
"author": {
|
|
34
|
+
"name": "granero",
|
|
35
|
+
"url": "https://balizame.com"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.10.0",
|
|
43
|
+
"tsx": "^4.7.0",
|
|
44
|
+
"typescript": "^5.3.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
],
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/granero/balizamcp.git"
|
|
57
|
+
},
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/granero/balizamcp/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://balizame.com"
|
|
62
|
+
}
|