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 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
+ [![npm version](https://badge.fury.io/js/balizamcp.svg)](https://www.npmjs.com/package/balizamcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io)
6
+ [![balizame.com](https://img.shields.io/badge/balizame.com-official-orange)](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)
@@ -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
+ }