@opendata.cat/mcp-server 0.0.8 → 0.0.10
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 +9 -7
- package/dist/clients/opendatasoft.d.ts +4 -0
- package/dist/clients/opendatasoft.js +20 -0
- package/dist/clients/socrata.js +6 -2
- package/dist/index.js +51 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,16 +18,18 @@ Un projecte d'**[opendata.cat](https://opendata.cat)** — associacio sense anim
|
|
|
18
18
|
|
|
19
19
|
## Portals disponibles
|
|
20
20
|
|
|
21
|
-
| Portal | Datasets |
|
|
22
|
-
|
|
23
|
-
| [Generalitat de Catalunya](https://analisi.transparenciacatalunya.cat) | 1.058 |
|
|
24
|
-
| [Ajuntament de Barcelona](https://opendata-ajuntament.barcelona.cat) | 555 |
|
|
25
|
-
| [Diputacio de Barcelona](https://dadesobertes.diba.cat) | 90 |
|
|
26
|
-
| [Consorci AOC](https://dadesobertes.seu-e.cat) | ~893 |
|
|
21
|
+
| Portal | Datasets | APIs |
|
|
22
|
+
|--------|----------|------|
|
|
23
|
+
| [Generalitat de Catalunya](https://analisi.transparenciacatalunya.cat) | 1.058 | Socrata (SoQL) |
|
|
24
|
+
| [Ajuntament de Barcelona](https://opendata-ajuntament.barcelona.cat) | 555 | CKAN datastore |
|
|
25
|
+
| [Diputacio de Barcelona](https://dadesobertes.diba.cat) | 90 | REST + JSON:API (CIDO) |
|
|
26
|
+
| [Consorci AOC](https://dadesobertes.seu-e.cat) | ~893 | CKAN datastore |
|
|
27
|
+
| [Ajuntament de Reus](https://opendata.reus.cat) | 119 | CKAN datastore |
|
|
28
|
+
| [Ajuntament de Girona](https://www.girona.cat/opendata/) | 53 | CKAN datastore |
|
|
27
29
|
|
|
28
30
|
El Consorci AOC inclou datasets de les **diputacions de Tarragona, Girona i Lleida**, ajuntaments, consells comarcals i altres organismes publics catalans.
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
**+2.700 datasets** de 6 portals. La majoria queryables amb filtres, cerca i paginacio.
|
|
31
33
|
|
|
32
34
|
El cataleg s'actualitza automaticament cada setmana. Cada endpoint es valida per assegurar que funciona.
|
|
33
35
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export async function queryOpendatasoft(endpoint, filters, search, limit = 20, offset = 0) {
|
|
2
|
+
const url = new URL(endpoint);
|
|
3
|
+
if (filters && Object.keys(filters).length > 0) {
|
|
4
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
5
|
+
url.searchParams.set(`refine.${key}`, value);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
if (search)
|
|
9
|
+
url.searchParams.set("q", search);
|
|
10
|
+
url.searchParams.set("rows", String(Math.min(limit, 100)));
|
|
11
|
+
url.searchParams.set("start", String(offset));
|
|
12
|
+
const resp = await fetch(url.toString());
|
|
13
|
+
if (!resp.ok)
|
|
14
|
+
throw new Error(`Opendatasoft error ${resp.status}: ${resp.statusText}`);
|
|
15
|
+
const data = (await resp.json());
|
|
16
|
+
return {
|
|
17
|
+
records: data.records.map((r) => r.fields),
|
|
18
|
+
total: data.nhits,
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/clients/socrata.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export async function querySocrata(endpoint, filters, search, limit = 20, offset = 0) {
|
|
2
2
|
const url = new URL(endpoint);
|
|
3
3
|
if (filters && Object.keys(filters).length > 0) {
|
|
4
|
-
const clauses = Object.entries(filters)
|
|
5
|
-
|
|
4
|
+
const clauses = Object.entries(filters)
|
|
5
|
+
.filter(([key]) => /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(key))
|
|
6
|
+
.map(([key, value]) => `\`${key}\`='${value.replace(/'/g, "''")}'`);
|
|
7
|
+
if (clauses.length > 0) {
|
|
8
|
+
url.searchParams.set("$where", clauses.join(" AND "));
|
|
9
|
+
}
|
|
6
10
|
}
|
|
7
11
|
if (search)
|
|
8
12
|
url.searchParams.set("$q", search);
|
package/dist/index.js
CHANGED
|
@@ -9,14 +9,15 @@ import { querySocrata } from "./clients/socrata.js";
|
|
|
9
9
|
import { queryCkan } from "./clients/ckan.js";
|
|
10
10
|
import { queryDiba } from "./clients/diba.js";
|
|
11
11
|
import { queryCido } from "./clients/cido.js";
|
|
12
|
+
import { queryOpendatasoft } from "./clients/opendatasoft.js";
|
|
12
13
|
const server = new McpServer({
|
|
13
14
|
name: "opendata-cat",
|
|
14
|
-
version: "0.0.
|
|
15
|
+
version: "0.0.10",
|
|
15
16
|
});
|
|
16
17
|
// Tool 1: search_datasets
|
|
17
18
|
server.tool("search_datasets", "Cerca datasets de dades obertes catalanes per text lliure. Retorna nom, descripció, portal i formats.", {
|
|
18
19
|
query: z.string().describe("Text de cerca (ex: 'qualitat aire', 'pressupostos')"),
|
|
19
|
-
portal: z.string().optional().describe("Filtrar per portal: 'generalitat', 'barcelona'
|
|
20
|
+
portal: z.string().optional().describe("Filtrar per portal: 'generalitat', 'barcelona', 'diba', 'aoc', 'reus', 'girona', 'fgc'"),
|
|
20
21
|
category: z.string().optional().describe("Filtrar per categoria"),
|
|
21
22
|
limit: z.number().optional().default(20).describe("Nombre màxim de resultats (defecte: 20)"),
|
|
22
23
|
}, async ({ query, portal, category, limit }) => {
|
|
@@ -89,6 +90,10 @@ server.tool("query_dataset", "Executa una consulta contra un dataset i retorna f
|
|
|
89
90
|
const data = await queryCkan(dataset.api_endpoint, filters, search, limit, offset);
|
|
90
91
|
results = data.records;
|
|
91
92
|
}
|
|
93
|
+
else if (dataset.api_type === "opendatasoft") {
|
|
94
|
+
const data = await queryOpendatasoft(dataset.api_endpoint, filters, search, limit, offset);
|
|
95
|
+
results = data.records;
|
|
96
|
+
}
|
|
92
97
|
else {
|
|
93
98
|
return {
|
|
94
99
|
content: [{
|
|
@@ -116,21 +121,57 @@ server.tool("query_dataset", "Executa una consulta contra un dataset i retorna f
|
|
|
116
121
|
// Tool 5: list_portals
|
|
117
122
|
server.tool("list_portals", "Llista els portals de dades obertes catalans disponibles amb estadístiques.", {}, async () => {
|
|
118
123
|
const portals = [
|
|
119
|
-
{ id: "generalitat", name: "Generalitat de Catalunya", url: "https://analisi.transparenciacatalunya.cat",
|
|
120
|
-
{ id: "barcelona", name: "Ajuntament de Barcelona", url: "https://opendata-ajuntament.barcelona.cat",
|
|
121
|
-
{ id: "diba", name: "Diputació de Barcelona", url: "https://dadesobertes.diba.cat",
|
|
124
|
+
{ id: "generalitat", name: "Generalitat de Catalunya", url: "https://analisi.transparenciacatalunya.cat", api: "Socrata" },
|
|
125
|
+
{ id: "barcelona", name: "Ajuntament de Barcelona", url: "https://opendata-ajuntament.barcelona.cat", api: "CKAN" },
|
|
126
|
+
{ id: "diba", name: "Diputació de Barcelona", url: "https://dadesobertes.diba.cat", api: "CKAN" },
|
|
127
|
+
{ id: "aoc", name: "Consorci AOC (diputacions, ajuntaments, consells comarcals)", url: "https://dadesobertes.seu-e.cat", api: "CKAN" },
|
|
128
|
+
{ id: "reus", name: "Ajuntament de Reus", url: "https://opendata.reus.cat", api: "CKAN" },
|
|
129
|
+
{ id: "girona", name: "Ajuntament de Girona", url: "https://www.girona.cat/opendata/", api: "CKAN" },
|
|
130
|
+
{ id: "fgc", name: "Ferrocarrils de la Generalitat de Catalunya", url: "https://dadesobertes.fgc.cat", api: "Opendatasoft" },
|
|
122
131
|
];
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
const cats = await getCategories();
|
|
133
|
+
const portalCounts = new Map(cats.portals.map((p) => [p.portal_id, p.total]));
|
|
134
|
+
const result = portals.map((p) => ({
|
|
135
|
+
...p,
|
|
136
|
+
dataset_count: portalCounts.get(p.id) ?? 0,
|
|
126
137
|
}));
|
|
127
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
138
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
128
139
|
});
|
|
129
140
|
// Tool 6: list_categories
|
|
130
141
|
server.tool("list_categories", "Llista totes les categories i temes de datasets disponibles amb comptadors per portal. Útil per saber quins tipus de dades hi ha.", {}, async () => {
|
|
131
142
|
const cats = await getCategories();
|
|
132
143
|
return { content: [{ type: "text", text: JSON.stringify(cats, null, 2) }] };
|
|
133
144
|
});
|
|
145
|
+
// Tool 7: related_datasets
|
|
146
|
+
server.tool("related_datasets", "Retorna datasets relacionats d'ALTRES portals. Ideal per descobrir dades complementàries.", {
|
|
147
|
+
dataset_id: z.string().describe("ID del dataset del qual vols trobar relacionats"),
|
|
148
|
+
}, async ({ dataset_id }) => {
|
|
149
|
+
const dataset = await getDatasetInfo(dataset_id);
|
|
150
|
+
if (!dataset) {
|
|
151
|
+
return { content: [{ type: "text", text: `Dataset '${dataset_id}' no trobat.` }] };
|
|
152
|
+
}
|
|
153
|
+
// Fetch related from API (stored in DB by enrichment script)
|
|
154
|
+
const resp = await fetch(`https://opendata.cat/api/dataset.php?id=${encodeURIComponent(dataset_id)}`);
|
|
155
|
+
if (!resp.ok) {
|
|
156
|
+
return { content: [{ type: "text", text: "Error obtenint relacions." }] };
|
|
157
|
+
}
|
|
158
|
+
const full = await resp.json();
|
|
159
|
+
const related = full.related ?? [];
|
|
160
|
+
if (!related.length) {
|
|
161
|
+
return { content: [{ type: "text", text: `No hi ha datasets relacionats per a '${dataset.name}'.` }] };
|
|
162
|
+
}
|
|
163
|
+
// Enrich with names
|
|
164
|
+
const details = await Promise.all(related.slice(0, 5).map(async (r) => {
|
|
165
|
+
const info = await getDatasetInfo(r.id);
|
|
166
|
+
return info ? { dataset_id: r.id, name: info.name, portal: info.portal_id, category: info.category, similarity: r.score } : null;
|
|
167
|
+
}));
|
|
168
|
+
return {
|
|
169
|
+
content: [{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: JSON.stringify({ dataset: dataset.name, related: details.filter(Boolean) }, null, 2),
|
|
172
|
+
}],
|
|
173
|
+
};
|
|
174
|
+
});
|
|
134
175
|
async function main() {
|
|
135
176
|
const mode = process.argv.includes("--http") ? "http" : "stdio";
|
|
136
177
|
const port = parseInt(process.env.MCP_PORT || "3100", 10);
|
|
@@ -149,7 +190,7 @@ async function main() {
|
|
|
149
190
|
// Health check
|
|
150
191
|
if (req.url === "/health") {
|
|
151
192
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
152
|
-
res.end(JSON.stringify({ status: "ok", name: "opendata-cat", version: "0.0.
|
|
193
|
+
res.end(JSON.stringify({ status: "ok", name: "opendata-cat", version: "0.0.10" }));
|
|
153
194
|
return;
|
|
154
195
|
}
|
|
155
196
|
// MCP endpoint
|