@opendata.cat/mcp-server 0.0.12 → 0.0.14
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/README.md +25 -0
- package/dist/clients/gtfsrt.d.ts +9 -0
- package/dist/clients/gtfsrt.js +92 -0
- package/dist/index.js +22 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -193,6 +193,24 @@ Usuari → LLM → MCP opendata.cat → API opendata.cat (cataleg)
|
|
|
193
193
|
|
|
194
194
|
No emmagatzema ni fa de proxy de dades. Cada consulta va directament a la font oficial.
|
|
195
195
|
|
|
196
|
+
## API REST
|
|
197
|
+
|
|
198
|
+
A mes del servidor MCP, opendata.cat ofereix una API REST publica per accedir al cataleg de datasets:
|
|
199
|
+
|
|
200
|
+
| Endpoint | Descripcio |
|
|
201
|
+
|----------|-----------|
|
|
202
|
+
| `GET /api/datasets.php?q=...` | Cerca datasets per text lliure |
|
|
203
|
+
| `GET /api/dataset.php?id=...` | Detall complet d'un dataset |
|
|
204
|
+
| `GET /api/categories.php` | Categories i portals amb comptadors |
|
|
205
|
+
| `GET /api/portals.php` | Portals de transparencia (1.769) |
|
|
206
|
+
| `GET /api/stats.php` | Estadistiques agregades |
|
|
207
|
+
| `GET /api/mcp-stats.php` | Metriques d'us del MCP |
|
|
208
|
+
| `POST /api/mcp` | Servidor MCP (Streamable HTTP) |
|
|
209
|
+
|
|
210
|
+
Documentacio interactiva (Swagger): **[opendata.cat/api/docs.html](https://opendata.cat/api/docs.html)**
|
|
211
|
+
|
|
212
|
+
Especificacio OpenAPI: [`opendata.cat/api/openapi.json`](https://opendata.cat/api/openapi.json)
|
|
213
|
+
|
|
196
214
|
## Sobre opendata.cat
|
|
197
215
|
|
|
198
216
|
[opendata.cat](https://opendata.cat) es una associacio catalana sense anim de lucre fundada el 2012 (registre 47468) dedicada a promoure la transparencia i l'acces a la informacio publica. Treballa en tres eixos: **estandarditzacio** de formats i protocols, **formacio** especialitzada per a professionals i administracions, i **collaboracio** publico-privada per a l'obertura de dades.
|
|
@@ -206,6 +224,13 @@ Les contribucions son benvingudes! Per afegir un nou portal de dades obertes:
|
|
|
206
224
|
|
|
207
225
|
## Changelog
|
|
208
226
|
|
|
227
|
+
### v0.0.13 (2026-04-14)
|
|
228
|
+
- Decodificador GTFS-RT integrat: trens FGC en temps real ara retornen dades reals (retards, alertes, posicions GPS)
|
|
229
|
+
- Descodifica automaticament fitxers Protocol Buffers (.pb) de GTFS Realtime
|
|
230
|
+
- API REST documentada amb Swagger UI a /api/docs.html (OpenAPI 3.1)
|
|
231
|
+
- Instruccions per a models locals: LM Studio, Ollama (MCPHost) i Jan
|
|
232
|
+
- Recomanacions de models oberts en catala (Softcatala): Qwen 3.5 9B, Gemma 3 12B
|
|
233
|
+
|
|
209
234
|
### v0.0.12 (2026-04-13)
|
|
210
235
|
- 14 prompts predefinits (8 analisi + 6 descobriment)
|
|
211
236
|
- Nous prompts: novetats, datasets_populars, explorar_portal, dades_municipi, datasets_temps_real, resum_portals
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Descarrega i decodifica un fitxer GTFS-RT (.pb) des d'Opendatasoft.
|
|
3
|
+
* Retorna dades estructurades: trip updates, alertes o posicions de vehicles.
|
|
4
|
+
*/
|
|
5
|
+
export declare function decodeGtfsRt(endpoint: string, limit: number): Promise<{
|
|
6
|
+
type: string;
|
|
7
|
+
count: number;
|
|
8
|
+
data: Record<string, unknown>[];
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import GtfsRealtimeBindings from "gtfs-realtime-bindings";
|
|
2
|
+
/**
|
|
3
|
+
* Descarrega i decodifica un fitxer GTFS-RT (.pb) des d'Opendatasoft.
|
|
4
|
+
* Retorna dades estructurades: trip updates, alertes o posicions de vehicles.
|
|
5
|
+
*/
|
|
6
|
+
export async function decodeGtfsRt(endpoint, limit) {
|
|
7
|
+
// 1. Obtenir file ID del record ODS
|
|
8
|
+
const resp = await fetch(endpoint + (endpoint.includes("?") ? "&" : "?") + "rows=1");
|
|
9
|
+
if (!resp.ok)
|
|
10
|
+
throw new Error(`ODS error ${resp.status}`);
|
|
11
|
+
const odsData = (await resp.json());
|
|
12
|
+
const file = odsData.records?.[0]?.fields?.file;
|
|
13
|
+
if (!file?.id)
|
|
14
|
+
throw new Error("No s'ha trobat el fitxer .pb");
|
|
15
|
+
// 2. Construir URL de descàrrega
|
|
16
|
+
const dsMatch = endpoint.match(/dataset=([^&]+)/);
|
|
17
|
+
const baseMatch = endpoint.match(/(https?:\/\/[^/]+)/);
|
|
18
|
+
if (!dsMatch || !baseMatch)
|
|
19
|
+
throw new Error("No s'ha pogut construir la URL de descàrrega");
|
|
20
|
+
const downloadUrl = `${baseMatch[1]}/explore/dataset/${dsMatch[1]}/files/${file.id}/download/`;
|
|
21
|
+
// 3. Descarregar i decodificar protobuf
|
|
22
|
+
const pbResp = await fetch(downloadUrl);
|
|
23
|
+
if (!pbResp.ok)
|
|
24
|
+
throw new Error(`Error descarregant .pb: ${pbResp.status}`);
|
|
25
|
+
const buf = Buffer.from(await pbResp.arrayBuffer());
|
|
26
|
+
const feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(buf);
|
|
27
|
+
// 4. Parsejar entitats segons tipus
|
|
28
|
+
const entities = feed.entity.slice(0, limit);
|
|
29
|
+
if (entities.some((e) => e.tripUpdate)) {
|
|
30
|
+
return {
|
|
31
|
+
type: "trip_updates",
|
|
32
|
+
count: feed.entity.length,
|
|
33
|
+
data: entities.filter((e) => e.tripUpdate).map((e) => {
|
|
34
|
+
const tu = e.tripUpdate;
|
|
35
|
+
const delays = (tu.stopTimeUpdate || []).map((su) => ({
|
|
36
|
+
stop_id: su.stopId,
|
|
37
|
+
arrival_delay_seconds: su.arrival?.delay ?? null,
|
|
38
|
+
departure_delay_seconds: su.departure?.delay ?? null,
|
|
39
|
+
}));
|
|
40
|
+
return {
|
|
41
|
+
trip_id: tu.trip?.tripId ?? null,
|
|
42
|
+
route_id: tu.trip?.routeId ?? null,
|
|
43
|
+
start_date: tu.trip?.startDate ?? null,
|
|
44
|
+
start_time: tu.trip?.startTime ?? null,
|
|
45
|
+
stops: delays,
|
|
46
|
+
max_delay_seconds: Math.max(0, ...delays.map((d) => d.arrival_delay_seconds ?? d.departure_delay_seconds ?? 0)),
|
|
47
|
+
};
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (entities.some((e) => e.alert)) {
|
|
52
|
+
return {
|
|
53
|
+
type: "alerts",
|
|
54
|
+
count: feed.entity.length,
|
|
55
|
+
data: entities.filter((e) => e.alert).map((e) => {
|
|
56
|
+
const a = e.alert;
|
|
57
|
+
return {
|
|
58
|
+
header: a.headerText?.translation?.[0]?.text ?? null,
|
|
59
|
+
description: a.descriptionText?.translation?.[0]?.text ?? null,
|
|
60
|
+
cause: a.cause ?? null,
|
|
61
|
+
effect: a.effect ?? null,
|
|
62
|
+
active_periods: (a.activePeriod || []).map((p) => ({
|
|
63
|
+
start: p.start ? new Date(Number(p.start) * 1000).toISOString() : null,
|
|
64
|
+
end: p.end ? new Date(Number(p.end) * 1000).toISOString() : null,
|
|
65
|
+
})),
|
|
66
|
+
routes: (a.informedEntity || []).map((ie) => ie.routeId).filter(Boolean),
|
|
67
|
+
stops: (a.informedEntity || []).map((ie) => ie.stopId).filter(Boolean),
|
|
68
|
+
};
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (entities.some((e) => e.vehicle)) {
|
|
73
|
+
return {
|
|
74
|
+
type: "vehicle_positions",
|
|
75
|
+
count: feed.entity.length,
|
|
76
|
+
data: entities.filter((e) => e.vehicle).map((e) => {
|
|
77
|
+
const v = e.vehicle;
|
|
78
|
+
return {
|
|
79
|
+
trip_id: v.trip?.tripId ?? null,
|
|
80
|
+
route_id: v.trip?.routeId ?? null,
|
|
81
|
+
latitude: v.position?.latitude ?? null,
|
|
82
|
+
longitude: v.position?.longitude ?? null,
|
|
83
|
+
speed_kmh: v.position?.speed != null ? Math.round(v.position.speed * 3.6) : null,
|
|
84
|
+
bearing: v.position?.bearing ?? null,
|
|
85
|
+
stop_id: v.stopId ?? null,
|
|
86
|
+
timestamp: v.timestamp ? new Date(Number(v.timestamp) * 1000).toISOString() : null,
|
|
87
|
+
};
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return { type: "unknown", count: feed.entity.length, data: [] };
|
|
92
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,9 +10,10 @@ import { queryCkan } from "./clients/ckan.js";
|
|
|
10
10
|
import { queryDiba } from "./clients/diba.js";
|
|
11
11
|
import { queryCido } from "./clients/cido.js";
|
|
12
12
|
import { queryOpendatasoft } from "./clients/opendatasoft.js";
|
|
13
|
+
import { decodeGtfsRt } from "./clients/gtfsrt.js";
|
|
13
14
|
const server = new McpServer({
|
|
14
15
|
name: "opendata-cat",
|
|
15
|
-
version: "0.0.
|
|
16
|
+
version: "0.0.13",
|
|
16
17
|
});
|
|
17
18
|
// Tool 1: search_datasets
|
|
18
19
|
server.tool("search_datasets", "Cerca datasets de dades obertes catalanes per text lliure. Retorna nom, descripció, portal i formats.", {
|
|
@@ -92,6 +93,25 @@ server.tool("query_dataset", "Executa una consulta contra un dataset i retorna f
|
|
|
92
93
|
}
|
|
93
94
|
else if (dataset.api_type === "opendatasoft") {
|
|
94
95
|
const data = await queryOpendatasoft(dataset.api_endpoint, filters, search, limit, offset);
|
|
96
|
+
// Detect and decode GTFS-RT protobuf files
|
|
97
|
+
const first = data.records[0];
|
|
98
|
+
const fileField = first?.file;
|
|
99
|
+
if (fileField?.filename?.endsWith(".pb") || fileField?.filename?.endsWith(".pbf")) {
|
|
100
|
+
const decoded = await decodeGtfsRt(dataset.api_endpoint, limit);
|
|
101
|
+
return {
|
|
102
|
+
content: [{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: JSON.stringify({
|
|
105
|
+
dataset: dataset.name,
|
|
106
|
+
format: "GTFS Realtime",
|
|
107
|
+
type: decoded.type,
|
|
108
|
+
total_entities: decoded.count,
|
|
109
|
+
count: decoded.data.length,
|
|
110
|
+
data: decoded.data,
|
|
111
|
+
}, null, 2),
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
95
115
|
results = data.records;
|
|
96
116
|
}
|
|
97
117
|
else {
|
|
@@ -451,7 +471,7 @@ async function main() {
|
|
|
451
471
|
// Health check
|
|
452
472
|
if (req.url === "/health") {
|
|
453
473
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
454
|
-
res.end(JSON.stringify({ status: "ok", name: "opendata-cat", version: "0.0.
|
|
474
|
+
res.end(JSON.stringify({ status: "ok", name: "opendata-cat", version: "0.0.13" }));
|
|
455
475
|
return;
|
|
456
476
|
}
|
|
457
477
|
// MCP endpoint
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.0.
|
|
6
|
+
"version": "0.0.14",
|
|
7
7
|
"description": "Servidor MCP per consultar les dades obertes públiques de Catalunya",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "dist/index.js",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"homepage": "https://github.com/xaviviro/Opendata.cat-MCP-Server#readme",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
|
+
"gtfs-realtime-bindings": "^1.1.1",
|
|
39
40
|
"zod": "^4.3.6"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|