@opendata.cat/mcp-server 0.0.13 → 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 +7 -0
- package/dist/clients/gtfsrt.d.ts +9 -0
- package/dist/clients/gtfsrt.js +92 -0
- package/dist/index.js +10 -10
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -224,6 +224,13 @@ Les contribucions son benvingudes! Per afegir un nou portal de dades obertes:
|
|
|
224
224
|
|
|
225
225
|
## Changelog
|
|
226
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
|
+
|
|
227
234
|
### v0.0.12 (2026-04-13)
|
|
228
235
|
- 14 prompts predefinits (8 analisi + 6 descobriment)
|
|
229
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,22 +93,21 @@ 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);
|
|
95
|
-
// Detect GTFS-RT protobuf files
|
|
96
|
+
// Detect and decode GTFS-RT protobuf files
|
|
96
97
|
const first = data.records[0];
|
|
97
98
|
const fileField = first?.file;
|
|
98
99
|
if (fileField?.filename?.endsWith(".pb") || fileField?.filename?.endsWith(".pbf")) {
|
|
99
|
-
const
|
|
100
|
-
const baseMatch = dataset.api_endpoint.match(/(https?:\/\/[^/]+)/);
|
|
101
|
-
const infoUrl = baseMatch ? `${baseMatch[1]}/explore/dataset/${dsMatch?.[1] ?? ""}/information/` : dataset.api_endpoint;
|
|
100
|
+
const decoded = await decodeGtfsRt(dataset.api_endpoint, limit);
|
|
102
101
|
return {
|
|
103
102
|
content: [{
|
|
104
103
|
type: "text",
|
|
105
104
|
text: JSON.stringify({
|
|
106
105
|
dataset: dataset.name,
|
|
107
|
-
format: "
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
format: "GTFS Realtime",
|
|
107
|
+
type: decoded.type,
|
|
108
|
+
total_entities: decoded.count,
|
|
109
|
+
count: decoded.data.length,
|
|
110
|
+
data: decoded.data,
|
|
111
111
|
}, null, 2),
|
|
112
112
|
}],
|
|
113
113
|
};
|
|
@@ -471,7 +471,7 @@ async function main() {
|
|
|
471
471
|
// Health check
|
|
472
472
|
if (req.url === "/health") {
|
|
473
473
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
474
|
-
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" }));
|
|
475
475
|
return;
|
|
476
476
|
}
|
|
477
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": {
|