@opendata.cat/mcp-server 0.0.1

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 ADDED
@@ -0,0 +1,155 @@
1
+ <p align="center">
2
+ <img src="banner.png" alt="opendata.cat MCP — Connecta el teu LLM amb les dades obertes de Catalunya" width="100%">
3
+ </p>
4
+
5
+ # Opendata.cat MCP Server
6
+
7
+ Servidor [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) que connecta els models de llenguatge (Claude, ChatGPT, Gemini...) amb les **dades obertes públiques de Catalunya**. Cerca datasets, explora metadades i consulta dades reals de la Generalitat, l'Ajuntament de Barcelona i la Diputació de Barcelona directament des del teu assistent d'IA.
8
+
9
+ Un projecte d'**[opendata.cat](https://opendata.cat)** — associació sense ànim de lucre fundada el 2012 que promou la transparència, la difusió i l'estandardització de les dades obertes a Catalunya. Organitza formacions, conferències i desenvolupa plataformes per facilitar l'accés a la informació pública.
10
+
11
+ ## Portals disponibles
12
+
13
+ | Portal | Datasets | API | Dades |
14
+ |--------|----------|-----|-------|
15
+ | [Generalitat de Catalunya](https://analisi.transparenciacatalunya.cat) | ~1.480 | Socrata | Medi ambient, salut, educació, economia, transport... |
16
+ | [Ajuntament de Barcelona](https://opendata-ajuntament.barcelona.cat) | ~1.036 | CKAN | Urbanisme, mobilitat, cultura, demografia, pressupostos... |
17
+ | [Diputació de Barcelona](https://dadesobertes.diba.cat) | ~124 | CKAN | Municipis, equipaments, patrimoni, energia, territori... |
18
+
19
+ **+2.600 datasets** actualitzats automàticament cada setmana.
20
+
21
+ ## Instal·lació ràpida
22
+
23
+ ### Claude Desktop
24
+
25
+ Afegeix al fitxer de configuració (`~/Library/Application Support/Claude/claude_desktop_config.json` a macOS o `%APPDATA%\Claude\claude_desktop_config.json` a Windows):
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "opendata-cat": {
31
+ "command": "npx",
32
+ "args": ["-y", "opendata-cat-mcp-server"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ### Claude Code (CLI)
39
+
40
+ ```bash
41
+ claude mcp add opendata-cat -- npx -y opendata-cat-mcp-server
42
+ ```
43
+
44
+ ### VS Code / Cursor
45
+
46
+ Afegeix al fitxer `.vscode/mcp.json` del teu projecte:
47
+
48
+ ```json
49
+ {
50
+ "servers": {
51
+ "opendata-cat": {
52
+ "command": "npx",
53
+ "args": ["-y", "opendata-cat-mcp-server"]
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Tools disponibles
60
+
61
+ | Tool | Descripció |
62
+ |------|-----------|
63
+ | `search_datasets` | Cerca datasets per text lliure al catàleg de +2.600 datasets |
64
+ | `get_dataset_info` | Retorna metadades completes: camps, tipus, llicència, endpoint |
65
+ | `list_dataset_fields` | Llista els camps d'un dataset amb nom, tipus i descripció |
66
+ | `query_dataset` | Consulta dades reals directament al portal origen |
67
+ | `list_portals` | Llista els portals disponibles amb estadístiques |
68
+ | `list_categories` | Llista categories i temes disponibles amb comptadors |
69
+
70
+ ### search_datasets
71
+
72
+ Cerca datasets per text lliure.
73
+
74
+ ```
75
+ query: "qualitat aire"
76
+ portal: "barcelona" # opcional: generalitat, barcelona, diba
77
+ category: "Medi Ambient" # opcional
78
+ limit: 20 # opcional (defecte: 20)
79
+ ```
80
+
81
+ ### get_dataset_info
82
+
83
+ Retorna totes les metadades d'un dataset.
84
+
85
+ ```
86
+ dataset_id: "generalitat:gn9e-3qhr"
87
+ ```
88
+
89
+ ### list_dataset_fields
90
+
91
+ Llista els camps d'un dataset amb nom, tipus i descripció.
92
+
93
+ ```
94
+ dataset_id: "generalitat:gn9e-3qhr"
95
+ ```
96
+
97
+ ### query_dataset
98
+
99
+ Executa una consulta directament contra el portal origen i retorna dades reals.
100
+
101
+ ```
102
+ dataset_id: "generalitat:gn9e-3qhr"
103
+ filters: {"estaci": "Sau"} # opcional
104
+ search: "embassament" # opcional
105
+ limit: 20 # opcional (defecte: 20, màx: 100)
106
+ offset: 0 # opcional
107
+ ```
108
+
109
+ ### list_portals
110
+
111
+ Llista els portals disponibles amb el nombre de datasets de cadascun. No requereix paràmetres.
112
+
113
+ ### list_categories
114
+
115
+ Llista totes les categories i temes de datasets disponibles amb comptadors per portal. Ideal per descobrir quins tipus de dades hi ha.
116
+
117
+ ## Exemples d'ús
118
+
119
+ Un cop configurat, pots fer preguntes al teu LLM com:
120
+
121
+ - *"Quins datasets hi ha sobre mobilitat a Barcelona?"*
122
+ - *"Mostra'm les dades de qualitat de l'aire d'ahir"*
123
+ - *"Quants equipaments culturals té Girona?"*
124
+ - *"Dona'm les últimes dades de pressupostos municipals"*
125
+ - *"Quin és l'estat dels embassaments de Catalunya?"*
126
+ - *"Compara les dades de transport públic entre Barcelona i la Diputació"*
127
+ - *"Quines dades obertes hi ha sobre educació a Catalunya?"*
128
+
129
+ ## Com funciona
130
+
131
+ ```
132
+ Usuari → LLM → MCP opendata.cat → API opendata.cat (catàleg)
133
+ → Portal origen (dades reals)
134
+ ```
135
+
136
+ 1. L'MCP consulta l'[API d'opendata.cat](https://opendata.cat) per descobrir datasets rellevants
137
+ 2. Quan l'usuari vol dades concretes, l'MCP fa la consulta directament al portal origen (Socrata o CKAN)
138
+ 3. Les dades tornen a l'LLM, que les interpreta i presenta a l'usuari
139
+
140
+ No emmagatzema ni fa de proxy de dades. Cada consulta va directament a la font oficial.
141
+
142
+ ## Sobre opendata.cat
143
+
144
+ [opendata.cat](https://opendata.cat) és una associació catalana sense ànim de lucre fundada el 2012 (registre 47468) dedicada a promoure la transparència i l'accés a la informació pública. Treballa en tres eixos: **estandardització** de formats i protocols, **formació** especialitzada per a professionals i administracions, i **col·laboració** público-privada per a l'obertura de dades.
145
+
146
+ ## Contribuir
147
+
148
+ Les contribucions són benvingudes! Per afegir un nou portal de dades obertes:
149
+
150
+ 1. Obre una [issue](https://github.com/xaviviro/Opendata.cat-MCP-Server/issues) amb la URL del portal i el tipus d'API
151
+ 2. O envia un pull request
152
+
153
+ ## Llicència
154
+
155
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ export interface DatasetSummary {
2
+ dataset_id: string;
3
+ portal_id: string;
4
+ name: string;
5
+ description: string;
6
+ category: string;
7
+ formats: string[];
8
+ api_type: string;
9
+ }
10
+ export interface DatasetDetail {
11
+ dataset_id: string;
12
+ portal_id: string;
13
+ name: string;
14
+ description: string;
15
+ category: string;
16
+ tags: string[];
17
+ api_type: "socrata" | "ckan";
18
+ api_endpoint: string;
19
+ formats: string[];
20
+ fields: {
21
+ name: string;
22
+ type: string;
23
+ description: string;
24
+ }[];
25
+ row_count: number | null;
26
+ last_updated: string;
27
+ license: string;
28
+ }
29
+ export interface SearchResult {
30
+ items: DatasetSummary[];
31
+ total: number;
32
+ limit: number;
33
+ offset: number;
34
+ }
35
+ export declare function searchDatasets(query: string, portal?: string, category?: string, limit?: number, offset?: number): Promise<SearchResult>;
36
+ export interface CategoriesResult {
37
+ total_datasets: number;
38
+ portals: {
39
+ portal_id: string;
40
+ total: number;
41
+ }[];
42
+ categories: {
43
+ portal_id: string;
44
+ category: string;
45
+ total: number;
46
+ }[];
47
+ }
48
+ export declare function getCategories(): Promise<CategoriesResult>;
49
+ export declare function getDatasetInfo(datasetId: string): Promise<DatasetDetail | null>;
package/dist/api.js ADDED
@@ -0,0 +1,30 @@
1
+ const API_BASE = "https://opendata.cat/api";
2
+ export async function searchDatasets(query, portal, category, limit = 20, offset = 0) {
3
+ const params = new URLSearchParams();
4
+ if (query)
5
+ params.set("q", query);
6
+ if (portal)
7
+ params.set("portal", portal);
8
+ if (category)
9
+ params.set("category", category);
10
+ params.set("limit", String(limit));
11
+ params.set("offset", String(offset));
12
+ const resp = await fetch(`${API_BASE}/datasets.php?${params}`);
13
+ if (!resp.ok)
14
+ throw new Error(`API error ${resp.status}`);
15
+ return (await resp.json());
16
+ }
17
+ export async function getCategories() {
18
+ const resp = await fetch(`${API_BASE}/categories.php`);
19
+ if (!resp.ok)
20
+ throw new Error(`API error ${resp.status}`);
21
+ return (await resp.json());
22
+ }
23
+ export async function getDatasetInfo(datasetId) {
24
+ const resp = await fetch(`${API_BASE}/dataset.php?id=${encodeURIComponent(datasetId)}`);
25
+ if (resp.status === 404)
26
+ return null;
27
+ if (!resp.ok)
28
+ throw new Error(`API error ${resp.status}`);
29
+ return (await resp.json());
30
+ }
@@ -0,0 +1,4 @@
1
+ export declare function queryCkan(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 queryCkan(endpoint, filters, search, limit = 20, offset = 0) {
2
+ const url = new URL(endpoint);
3
+ if (filters && Object.keys(filters).length > 0) {
4
+ url.searchParams.set("filters", JSON.stringify(filters));
5
+ }
6
+ if (search)
7
+ url.searchParams.set("q", search);
8
+ url.searchParams.set("limit", String(Math.min(limit, 100)));
9
+ url.searchParams.set("offset", String(offset));
10
+ const resp = await fetch(url.toString());
11
+ if (!resp.ok)
12
+ throw new Error(`CKAN error ${resp.status}: ${resp.statusText}`);
13
+ const data = await resp.json();
14
+ if (!data.success)
15
+ throw new Error(`CKAN error: ${JSON.stringify(data.error)}`);
16
+ return {
17
+ records: data.result.records ?? [],
18
+ total: data.result.total ?? 0,
19
+ };
20
+ }
@@ -0,0 +1 @@
1
+ export declare function querySocrata(endpoint: string, filters?: Record<string, string>, search?: string, limit?: number, offset?: number): Promise<Record<string, unknown>[]>;
@@ -0,0 +1,15 @@
1
+ export async function querySocrata(endpoint, filters, search, limit = 20, offset = 0) {
2
+ const url = new URL(endpoint);
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 "));
6
+ }
7
+ if (search)
8
+ url.searchParams.set("$q", search);
9
+ url.searchParams.set("$limit", String(Math.min(limit, 100)));
10
+ url.searchParams.set("$offset", String(offset));
11
+ const resp = await fetch(url.toString());
12
+ if (!resp.ok)
13
+ throw new Error(`Socrata error ${resp.status}: ${resp.statusText}`);
14
+ return (await resp.json());
15
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { searchDatasets, getDatasetInfo, getCategories } from "./api.js";
6
+ import { querySocrata } from "./clients/socrata.js";
7
+ import { queryCkan } from "./clients/ckan.js";
8
+ const server = new McpServer({
9
+ name: "opendata-cat",
10
+ version: "0.0.1",
11
+ });
12
+ // Tool 1: search_datasets
13
+ server.tool("search_datasets", "Cerca datasets de dades obertes catalanes per text lliure. Retorna nom, descripció, portal i formats.", {
14
+ query: z.string().describe("Text de cerca (ex: 'qualitat aire', 'pressupostos')"),
15
+ portal: z.string().optional().describe("Filtrar per portal: 'generalitat', 'barcelona' o 'diba'"),
16
+ category: z.string().optional().describe("Filtrar per categoria"),
17
+ limit: z.number().optional().default(20).describe("Nombre màxim de resultats (defecte: 20)"),
18
+ }, async ({ query, portal, category, limit }) => {
19
+ const result = await searchDatasets(query, portal, category, limit);
20
+ return {
21
+ content: [{
22
+ type: "text",
23
+ text: JSON.stringify(result, null, 2),
24
+ }],
25
+ };
26
+ });
27
+ // Tool 2: get_dataset_info
28
+ server.tool("get_dataset_info", "Retorna totes les metadades d'un dataset: camps, tipus, descripció, endpoint API, llicència.", {
29
+ dataset_id: z.string().describe("ID del dataset (ex: 'generalitat:gn9e-3qhr')"),
30
+ }, async ({ dataset_id }) => {
31
+ const dataset = await getDatasetInfo(dataset_id);
32
+ if (!dataset) {
33
+ return { content: [{ type: "text", text: `Dataset '${dataset_id}' no trobat.` }] };
34
+ }
35
+ return { content: [{ type: "text", text: JSON.stringify(dataset, null, 2) }] };
36
+ });
37
+ // Tool 3: list_dataset_fields
38
+ server.tool("list_dataset_fields", "Llista els camps d'un dataset amb el seu nom, tipus i descripció.", {
39
+ dataset_id: z.string().describe("ID del dataset"),
40
+ }, async ({ dataset_id }) => {
41
+ const dataset = await getDatasetInfo(dataset_id);
42
+ if (!dataset) {
43
+ return { content: [{ type: "text", text: `Dataset '${dataset_id}' no trobat.` }] };
44
+ }
45
+ return { content: [{ type: "text", text: JSON.stringify(dataset.fields, null, 2) }] };
46
+ });
47
+ // Tool 4: query_dataset
48
+ server.tool("query_dataset", "Executa una consulta contra un dataset i retorna files de dades reals del portal origen.", {
49
+ dataset_id: z.string().describe("ID del dataset a consultar"),
50
+ filters: z.record(z.string(), z.string()).optional().describe("Filtres clau-valor (ex: {\"ciutat\": \"Barcelona\"})"),
51
+ search: z.string().optional().describe("Cerca de text lliure dins el dataset"),
52
+ limit: z.number().optional().default(20).describe("Files a retornar (defecte: 20, màxim: 100)"),
53
+ offset: z.number().optional().default(0).describe("Desplaçament per paginació"),
54
+ }, async ({ dataset_id, filters, search, limit, offset }) => {
55
+ const dataset = await getDatasetInfo(dataset_id);
56
+ if (!dataset) {
57
+ return { content: [{ type: "text", text: `Dataset '${dataset_id}' no trobat.` }] };
58
+ }
59
+ try {
60
+ let results;
61
+ if (dataset.api_type === "socrata") {
62
+ results = await querySocrata(dataset.api_endpoint, filters, search, limit, offset);
63
+ }
64
+ else {
65
+ const data = await queryCkan(dataset.api_endpoint, filters, search, limit, offset);
66
+ results = data.records;
67
+ }
68
+ return {
69
+ content: [{
70
+ type: "text",
71
+ text: JSON.stringify({ dataset: dataset.name, count: results.length, data: results }, null, 2),
72
+ }],
73
+ };
74
+ }
75
+ catch (err) {
76
+ return {
77
+ content: [{
78
+ type: "text",
79
+ text: `Error consultant ${dataset.name}: ${err instanceof Error ? err.message : String(err)}`,
80
+ }],
81
+ };
82
+ }
83
+ });
84
+ // Tool 5: list_portals
85
+ server.tool("list_portals", "Llista els portals de dades obertes catalans disponibles amb estadístiques.", {}, async () => {
86
+ const portals = [
87
+ { id: "generalitat", name: "Generalitat de Catalunya", url: "https://analisi.transparenciacatalunya.cat", api_type: "socrata" },
88
+ { id: "barcelona", name: "Ajuntament de Barcelona", url: "https://opendata-ajuntament.barcelona.cat", api_type: "ckan" },
89
+ { id: "diba", name: "Diputació de Barcelona", url: "https://dadesobertes.diba.cat", api_type: "ckan" },
90
+ ];
91
+ const counts = await Promise.all(portals.map(async (p) => {
92
+ const result = await searchDatasets("", p.id, undefined, 1);
93
+ return { ...p, dataset_count: result.total };
94
+ }));
95
+ return { content: [{ type: "text", text: JSON.stringify(counts, null, 2) }] };
96
+ });
97
+ // Tool 6: list_categories
98
+ 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 () => {
99
+ const cats = await getCategories();
100
+ return { content: [{ type: "text", text: JSON.stringify(cats, null, 2) }] };
101
+ });
102
+ async function main() {
103
+ const transport = new StdioServerTransport();
104
+ await server.connect(transport);
105
+ }
106
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@opendata.cat/mcp-server",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.0.1",
7
+ "description": "Servidor MCP per consultar les dades obertes públiques de Catalunya",
8
+ "type": "module",
9
+ "main": "dist/index.js",
10
+ "bin": {
11
+ "opendata-cat-mcp": "dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist/"
19
+ ],
20
+ "keywords": [
21
+ "mcp",
22
+ "opendata",
23
+ "catalunya",
24
+ "dades-obertes"
25
+ ],
26
+ "author": "opendata.cat",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/xaviviro/Opendata.cat-MCP-Server.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/xaviviro/Opendata.cat-MCP-Server/issues"
34
+ },
35
+ "homepage": "https://github.com/xaviviro/Opendata.cat-MCP-Server#readme",
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.29.0",
38
+ "zod": "^4.3.6"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.6.0",
42
+ "typescript": "^6.0.2"
43
+ }
44
+ }