@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 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.12",
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 dsMatch = dataset.api_endpoint.match(/dataset=([^&]+)/);
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: "Protocol Buffers (GTFS-RT)",
108
- message: "Aquest dataset conté dades en format Protocol Buffers (.pb), un format binari per a transport públic en temps real (GTFS Realtime). No es pot llegir directament com a text/JSON. Per usar-lo cal un decoder GTFS-RT (ex: gtfs-realtime-bindings).",
109
- filename: fileField.filename,
110
- download_url: infoUrl,
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.12" }));
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.13",
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": {