@opendata.cat/mcp-server 0.0.9 → 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.
@@ -0,0 +1,4 @@
1
+ export declare function queryOpendatasoft(endpoint: string, filters?: Record<string, string>, search?: string, limit?: number, offset?: number): Promise<{
2
+ records: Record<string, unknown>[];
3
+ total: number;
4
+ }>;
@@ -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
+ }
@@ -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).map(([key, value]) => `${key}='${value.replace(/'/g, "''")}'`);
5
- url.searchParams.set("$where", clauses.join(" AND "));
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.9",
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' o 'diba'"),
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,15 +121,21 @@ 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", api_type: "socrata" },
120
- { id: "barcelona", name: "Ajuntament de Barcelona", url: "https://opendata-ajuntament.barcelona.cat", api_type: "ckan" },
121
- { id: "diba", name: "Diputació de Barcelona", url: "https://dadesobertes.diba.cat", api_type: "ckan" },
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 counts = await Promise.all(portals.map(async (p) => {
124
- const result = await searchDatasets("", p.id, undefined, 1);
125
- return { ...p, dataset_count: result.total };
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(counts, null, 2) }] };
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 () => {
@@ -179,7 +190,7 @@ async function main() {
179
190
  // Health check
180
191
  if (req.url === "/health") {
181
192
  res.writeHead(200, { "Content-Type": "application/json" });
182
- res.end(JSON.stringify({ status: "ok", name: "opendata-cat", version: "0.0.9" }));
193
+ res.end(JSON.stringify({ status: "ok", name: "opendata-cat", version: "0.0.10" }));
183
194
  return;
184
195
  }
185
196
  // MCP endpoint
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.0.9",
6
+ "version": "0.0.10",
7
7
  "description": "Servidor MCP per consultar les dades obertes públiques de Catalunya",
8
8
  "type": "module",
9
9
  "main": "dist/index.js",