mcp-chilegob-dataset 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gerard Bourguett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # mcp-chilegob-dataset
2
+
3
+ Servidor MCP que expone el portal de datos abiertos del gobierno de Chile — [datos.gob.cl](https://datos.gob.cl) — como herramientas para asistentes de inteligencia artificial.
4
+
5
+ Construido con [Hono](https://hono.dev) y el [SDK de TypeScript del Model Context Protocol](https://github.com/modelcontextprotocol/typescript-sdk).
6
+
7
+ ---
8
+
9
+ ## ¿Qué es MCP?
10
+
11
+ El **Model Context Protocol (MCP)** es un estándar abierto que permite a los asistentes de IA (Claude, etc.) conectarse a herramientas y fuentes de datos externas de manera estructurada y segura. En lugar de copiar datos crudos en una conversación, el asistente llama a una herramienta y recibe resultados estructurados.
12
+
13
+ ## ¿Qué es datos.gob.cl?
14
+
15
+ [datos.gob.cl](https://datos.gob.cl) es el portal oficial de datos abiertos del gobierno de Chile, impulsado por [CKAN](https://ckan.org). Contiene miles de datasets públicos de instituciones gubernamentales — salud, educación, transporte, medio ambiente, y más. Todos los datos son de acceso público, sin registro ni autenticación.
16
+
17
+ ---
18
+
19
+ ## Instalación y uso rápido
20
+
21
+ ### Con Claude Desktop (recomendado)
22
+
23
+ **Paso 1** — Abrí la configuración de Claude Desktop.
24
+
25
+ En Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
26
+ En Windows: `%APPDATA%\Claude\claude_desktop_config.json`
27
+
28
+ **Paso 2** — Agregá estas líneas:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "chilegob": {
34
+ "command": "npx",
35
+ "args": ["-y", "mcp-chilegob-dataset"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ **Paso 3** — Reiniciá Claude Desktop. La primera vez descarga el paquete automáticamente.
42
+
43
+ Eso es todo. Podés pedirle a Claude cosas como:
44
+
45
+ > *"Buscá datasets sobre educación en datos.gob.cl"*
46
+ > *"Mostrá los datos del dataset de matrícula universitaria"*
47
+ > *"¿Qué datasets de salud hay disponibles?"*
48
+
49
+ ---
50
+
51
+ ## Herramientas disponibles
52
+
53
+ Este servidor expone tres herramientas que el asistente puede usar automáticamente.
54
+
55
+ ### `search_datasets`
56
+
57
+ Busca datasets en datos.gob.cl por palabra clave. Devuelve una lista resumida.
58
+
59
+ **Parámetros:**
60
+
61
+ | Nombre | Tipo | Requerido | Por defecto | Descripción |
62
+ |--------|------|-----------|-------------|-------------|
63
+ | `query` | string | ✅ | — | Texto de búsqueda (en español o inglés) |
64
+ | `limit` | number | ❌ | 10 | Máximo de resultados (1–100) |
65
+
66
+ **Ejemplo de respuesta:**
67
+
68
+ ```json
69
+ [
70
+ {
71
+ "id": "matricula-en-educacion-superior",
72
+ "title": "Matrícula en Educación Superior",
73
+ "description": "Bases de datos de matrícula en el sistema de educación superior...",
74
+ "organization": "Subsecretaría de Educación",
75
+ "resource_count": 1
76
+ }
77
+ ]
78
+ ```
79
+
80
+ ---
81
+
82
+ ### `get_dataset`
83
+
84
+ Obtiene los metadatos completos de un dataset: descripción, recursos, etiquetas y licencia.
85
+
86
+ **Parámetros:**
87
+
88
+ | Nombre | Tipo | Requerido | Descripción |
89
+ |--------|------|-----------|-------------|
90
+ | `id` | string | ✅ | Slug o UUID del dataset (obtenido de `search_datasets`) |
91
+
92
+ **Ejemplo de respuesta:**
93
+
94
+ ```json
95
+ {
96
+ "id": "matricula-en-educacion-superior",
97
+ "title": "Matrícula en Educación Superior",
98
+ "organization": "Subsecretaría de Educación",
99
+ "license": "Creative Commons Non-Commercial (Any)",
100
+ "tags": ["educación", "educación superior", "matriculas"],
101
+ "resources": [
102
+ {
103
+ "id": "37377aff-6df8-4424-9228-2f44f3e67fcd",
104
+ "name": "Base de datos de matrícula",
105
+ "format": "CSV",
106
+ "url": "https://datosabiertos.mineduc.cl/...",
107
+ "datastore_available": true
108
+ }
109
+ ]
110
+ }
111
+ ```
112
+
113
+ > Verificá el campo `datastore_available` antes de usar `get_resource_data`. Los recursos sin datastore deben descargarse desde su `url`.
114
+
115
+ ---
116
+
117
+ ### `get_resource_data`
118
+
119
+ Lee filas tabulares de un recurso con el datastore CKAN habilitado. Soporta paginación.
120
+
121
+ **Parámetros:**
122
+
123
+ | Nombre | Tipo | Requerido | Por defecto | Descripción |
124
+ |--------|------|-----------|-------------|-------------|
125
+ | `resource_id` | string | ✅ | — | UUID del recurso (obtenido de `get_dataset`) |
126
+ | `limit` | number | ❌ | 50 | Filas a devolver (1–500) |
127
+ | `offset` | number | ❌ | 0 | Desplazamiento para paginación |
128
+
129
+ **Ejemplo de respuesta:**
130
+
131
+ ```json
132
+ {
133
+ "total": 42,
134
+ "returned": 3,
135
+ "offset": 0,
136
+ "fields": [
137
+ { "id": "Region", "type": "text" },
138
+ { "id": "Provincia", "type": "text" },
139
+ { "id": "Comuna", "type": "text" }
140
+ ],
141
+ "records": [
142
+ { "Region": "COQUIMBO", "Provincia": "ELQUI", "Comuna": "LA SERENA" }
143
+ ]
144
+ }
145
+ ```
146
+
147
+ > Si el recurso no tiene datastore habilitado, la herramienta devuelve un mensaje descriptivo indicando cómo acceder al archivo directamente.
148
+
149
+ ---
150
+
151
+ ## Para desarrolladores
152
+
153
+ ### Requisitos
154
+
155
+ - **Node.js 20 o superior**
156
+ - **npm**
157
+
158
+ ### Instalación desde el código fuente
159
+
160
+ ```bash
161
+ git clone https://github.com/gerardbourguett/mcp-chilegob-dataset.git
162
+ cd mcp-chilegob-dataset
163
+ npm install
164
+ ```
165
+
166
+ ### Scripts disponibles
167
+
168
+ | Comando | Descripción |
169
+ |---------|-------------|
170
+ | `npm run dev` | Inicia el servidor HTTP en `http://localhost:3000/mcp` con recarga automática |
171
+ | `npm run build` | Compila TypeScript a `dist/` |
172
+ | `npm start` | Inicia el servidor HTTP desde `dist/` |
173
+ | `npm run typecheck` | Verifica tipos sin compilar |
174
+
175
+ ### Probar localmente (MCP Inspector)
176
+
177
+ Con el servidor corriendo (`npm run dev`), abrí otra terminal y ejecutá:
178
+
179
+ ```bash
180
+ npx @modelcontextprotocol/inspector
181
+ ```
182
+
183
+ Se abre una interfaz web en `http://localhost:6274`. Configurá:
184
+ - **Transport type**: `Streamable HTTP`
185
+ - **URL**: `http://localhost:3000/mcp`
186
+
187
+ Desde ahí podés llamar a cualquier herramienta de forma interactiva y ver las respuestas.
188
+
189
+ ### Arquitectura
190
+
191
+ ```
192
+ Cliente AI → Hono HTTP → McpServer → CKAN API v3 (datos.gob.cl)
193
+ ```
194
+
195
+ ```
196
+ src/
197
+ ├── index.ts # Servidor HTTP (para desarrollo y despliegue remoto)
198
+ ├── stdio.ts # Transporte stdio (para npx y Claude Desktop)
199
+ ├── server.ts # Instancia McpServer + registro de herramientas
200
+ ├── ckan.ts # Cliente tipado para la API CKAN v3
201
+ └── tools/
202
+ ├── search.ts # search_datasets
203
+ ├── dataset.ts # get_dataset
204
+ └── resource.ts # get_resource_data
205
+ ```
206
+
207
+ ### Cómo agregar nuevas herramientas
208
+
209
+ 1. Creá `src/tools/tu-herramienta.ts`
210
+ 2. Importala y registrala en `src/server.ts`
211
+
212
+ ```typescript
213
+ // src/tools/tu-herramienta.ts
214
+ import type { McpServer } from '@modelcontextprotocol/server'
215
+ import { z } from 'zod'
216
+
217
+ export function registerTuHerramienta(server: McpServer): void {
218
+ server.registerTool(
219
+ 'nombre_herramienta',
220
+ {
221
+ title: 'Nombre visible en el cliente',
222
+ description: 'Descripción clara. El asistente la usa para decidir cuándo llamar esta herramienta.',
223
+ inputSchema: z.object({
224
+ parametro: z.string().describe('Descripción del parámetro'),
225
+ }),
226
+ },
227
+ async ({ parametro }) => {
228
+ // tu lógica acá
229
+ return {
230
+ content: [{ type: 'text', text: JSON.stringify({ resultado: parametro }) }],
231
+ }
232
+ }
233
+ )
234
+ }
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Limitaciones conocidas
240
+
241
+ - **Disponibilidad del datastore** — No todos los recursos tienen datastore habilitado en CKAN. Los archivos (CSV, XLS, PDF) deben descargarse desde su URL directa.
242
+ - **Sin caché** — Cada llamada hace una solicitud en vivo a datos.gob.cl. No hay límites de tasa documentados.
243
+ - **Paquetes en alpha** — `@modelcontextprotocol/hono` y `@modelcontextprotocol/server` están en versión alpha.
244
+
245
+ ---
246
+
247
+ ## Contribuciones
248
+
249
+ Las contribuciones son bienvenidas. Algunas ideas:
250
+
251
+ - [ ] Herramienta `list_organizations` — listar instituciones disponibles
252
+ - [ ] Herramienta `get_resource_schema` — tipos y descripciones de columnas
253
+ - [ ] Caché en memoria para reducir llamadas a la API
254
+ - [ ] MCP Resources con URI templates (`datos-gob-cl://dataset/{id}`)
255
+
256
+ Por favor, abrí un issue antes de enviar un PR grande.
257
+
258
+ ---
259
+
260
+ ## Licencia
261
+
262
+ MIT — ver [LICENSE](./LICENSE).
package/dist/ckan.js ADDED
@@ -0,0 +1,30 @@
1
+ const CKAN_BASE = 'https://datos.gob.cl/api/3/action';
2
+ async function ckanAction(action, params) {
3
+ const url = new URL(`${CKAN_BASE}/${action}`);
4
+ for (const [key, value] of Object.entries(params)) {
5
+ url.searchParams.set(key, String(value));
6
+ }
7
+ const response = await fetch(url.toString());
8
+ if (!response.ok) {
9
+ throw new Error(`CKAN API error: ${response.status} ${response.statusText}`);
10
+ }
11
+ const data = await response.json();
12
+ if (!data.success) {
13
+ throw new Error(`CKAN error: ${data.error?.message ?? 'Unknown error'}`);
14
+ }
15
+ return data.result;
16
+ }
17
+ export async function searchDatasets(query, limit = 10) {
18
+ const result = await ckanAction('package_search', { q: query, rows: limit });
19
+ return result.results;
20
+ }
21
+ export async function getDataset(id) {
22
+ return ckanAction('package_show', { id });
23
+ }
24
+ export async function getResourceData(resourceId, limit = 50, offset = 0) {
25
+ return ckanAction('datastore_search', {
26
+ resource_id: resourceId,
27
+ limit,
28
+ offset,
29
+ });
30
+ }
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { serve } from '@hono/node-server';
2
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';
3
+ import { createMcpHonoApp } from '@modelcontextprotocol/hono';
4
+ import { server } from './server.js';
5
+ const transport = new WebStandardStreamableHTTPServerTransport({
6
+ sessionIdGenerator: undefined,
7
+ });
8
+ await server.connect(transport);
9
+ const app = createMcpHonoApp();
10
+ app.all('/mcp', c => transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') }));
11
+ serve({ fetch: app.fetch, port: 3000 }, info => {
12
+ console.log(`MCP server running on http://localhost:${info.port}/mcp`);
13
+ });
package/dist/server.js ADDED
@@ -0,0 +1,11 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import { registerSearchTool } from './tools/search.js';
3
+ import { registerDatasetTool } from './tools/dataset.js';
4
+ import { registerResourceTool } from './tools/resource.js';
5
+ export const server = new McpServer({
6
+ name: 'datos-gob-cl',
7
+ version: '1.0.0',
8
+ });
9
+ registerSearchTool(server);
10
+ registerDatasetTool(server);
11
+ registerResourceTool(server);
package/dist/stdio.js ADDED
@@ -0,0 +1,4 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/server';
2
+ import { server } from './server.js';
3
+ const transport = new StdioServerTransport();
4
+ await server.connect(transport);
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ import { getDataset } from '../ckan.js';
3
+ export function registerDatasetTool(server) {
4
+ server.registerTool('get_dataset', {
5
+ title: 'Get Dataset',
6
+ description: 'Get full metadata for a dataset from datos.gob.cl by its ID or slug. Use search_datasets first to find the ID.',
7
+ inputSchema: z.object({
8
+ id: z.string().describe('Dataset slug or UUID (e.g., "nombre-del-dataset" or a UUID from search results)'),
9
+ }),
10
+ }, async ({ id }) => {
11
+ try {
12
+ const dataset = await getDataset(id);
13
+ return {
14
+ content: [{
15
+ type: 'text',
16
+ text: JSON.stringify({
17
+ id: dataset.name,
18
+ title: dataset.title,
19
+ description: dataset.notes,
20
+ organization: dataset.organization?.title ?? null,
21
+ license: dataset.license_title,
22
+ tags: dataset.tags.map(t => t.name),
23
+ resources: dataset.resources.map(r => ({
24
+ id: r.id,
25
+ name: r.name,
26
+ format: r.format,
27
+ url: r.url,
28
+ datastore_available: r.datastore_active,
29
+ })),
30
+ }, null, 2),
31
+ }],
32
+ };
33
+ }
34
+ catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ const isNotFound = message.toLowerCase().includes('not found');
37
+ return {
38
+ content: [{ type: 'text', text: isNotFound ? `Dataset not found: ${id}` : `Error: ${message}` }],
39
+ isError: true,
40
+ };
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ import { getResourceData } from '../ckan.js';
3
+ export function registerResourceTool(server) {
4
+ server.registerTool('get_resource_data', {
5
+ title: 'Get Resource Data',
6
+ description: 'Read tabular data from a CKAN resource. Only works for resources with datastore enabled (check datastore_available from get_dataset). Supports pagination via limit and offset.',
7
+ inputSchema: z.object({
8
+ resource_id: z.string().describe('Resource UUID from a dataset\'s resources list (use get_dataset to obtain it)'),
9
+ limit: z.number().int().min(1).max(500).default(50).optional().describe('Rows to return (default: 50, max: 500)'),
10
+ offset: z.number().int().min(0).default(0).optional().describe('Row offset for pagination (default: 0)'),
11
+ }),
12
+ }, async ({ resource_id, limit, offset }) => {
13
+ try {
14
+ const result = await getResourceData(resource_id, limit ?? 50, offset ?? 0);
15
+ return {
16
+ content: [{
17
+ type: 'text',
18
+ text: JSON.stringify({
19
+ total: result.total,
20
+ returned: result.records.length,
21
+ offset: offset ?? 0,
22
+ fields: result.fields,
23
+ records: result.records,
24
+ }, null, 2),
25
+ }],
26
+ };
27
+ }
28
+ catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ const isNoDatastore = message.toLowerCase().includes('datastore') || message.includes('404') || message.includes('NOT FOUND');
31
+ return {
32
+ content: [{
33
+ type: 'text',
34
+ text: isNoDatastore
35
+ ? `Datastore not available for resource "${resource_id}". This resource may be a file (CSV, XLS, PDF) without an activated datastore. Use the resource URL from get_dataset to download it directly.`
36
+ : `Error: ${message}`,
37
+ }],
38
+ isError: true,
39
+ };
40
+ }
41
+ });
42
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import { searchDatasets } from '../ckan.js';
3
+ export function registerSearchTool(server) {
4
+ server.registerTool('search_datasets', {
5
+ title: 'Search Datasets',
6
+ description: 'Search for datasets in the Chilean government open data portal (datos.gob.cl). Returns a list of matching datasets with their IDs, titles, and resource counts.',
7
+ inputSchema: z.object({
8
+ query: z.string().describe('Search query in Spanish or English (e.g., "educación", "salud", "transporte")'),
9
+ limit: z.number().int().min(1).max(100).default(10).optional().describe('Max number of results (default: 10, max: 100)'),
10
+ }),
11
+ }, async ({ query, limit }) => {
12
+ try {
13
+ const datasets = await searchDatasets(query, limit ?? 10);
14
+ const formatted = datasets.map(d => ({
15
+ id: d.name,
16
+ title: d.title,
17
+ description: d.notes?.slice(0, 200) ?? '',
18
+ organization: d.organization?.title ?? 'N/A',
19
+ resource_count: d.num_resources,
20
+ }));
21
+ return {
22
+ content: [{ type: 'text', text: JSON.stringify(formatted, null, 2) }],
23
+ };
24
+ }
25
+ catch (error) {
26
+ return {
27
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
28
+ isError: true,
29
+ };
30
+ }
31
+ });
32
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "mcp-chilegob-dataset",
3
+ "version": "0.1.0",
4
+ "description": "MCP server exposing Chile's open government dataset portal (datos.gob.cl / CKAN API v3)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/gerardbourguett/mcp-chilegob-dataset.git"
10
+ },
11
+ "keywords": [
12
+ "mcp",
13
+ "model-context-protocol",
14
+ "chile",
15
+ "open-data",
16
+ "datos-gob-cl",
17
+ "ckan",
18
+ "hono"
19
+ ],
20
+ "bin": {
21
+ "mcp-chilegob-dataset": "./dist/stdio.js"
22
+ },
23
+ "files": [
24
+ "dist/",
25
+ "src/",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "scripts": {
33
+ "dev": "tsx watch src/index.ts",
34
+ "build": "tsc",
35
+ "start": "node dist/index.js",
36
+ "typecheck": "tsc --noEmit",
37
+ "prepublishOnly": "tsc"
38
+ },
39
+ "dependencies": {
40
+ "@cfworker/json-schema": "^4.1.1",
41
+ "@hono/node-server": "^1.19.13",
42
+ "@modelcontextprotocol/hono": "^2.0.0-alpha.2",
43
+ "@modelcontextprotocol/server": "^2.0.0-alpha.2",
44
+ "hono": "^4.12.12",
45
+ "zod": "^4.3.6"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.11.17",
49
+ "tsx": "^4.7.1",
50
+ "typescript": "^5.8.3"
51
+ }
52
+ }
package/src/ckan.ts ADDED
@@ -0,0 +1,67 @@
1
+ const CKAN_BASE = 'https://datos.gob.cl/api/3/action'
2
+
3
+ export interface CkanDataset {
4
+ id: string
5
+ name: string
6
+ title: string
7
+ notes: string
8
+ organization: { title: string } | null
9
+ resources: CkanResource[]
10
+ tags: { name: string }[]
11
+ license_title: string
12
+ num_resources: number
13
+ }
14
+
15
+ export interface CkanResource {
16
+ id: string
17
+ name: string
18
+ format: string
19
+ url: string
20
+ datastore_active: boolean
21
+ }
22
+
23
+ export interface CkanDatastoreResult {
24
+ fields: { id: string; type: string }[]
25
+ records: Record<string, unknown>[]
26
+ total: number
27
+ }
28
+
29
+ async function ckanAction<T>(action: string, params: Record<string, unknown>): Promise<T> {
30
+ const url = new URL(`${CKAN_BASE}/${action}`)
31
+ for (const [key, value] of Object.entries(params)) {
32
+ url.searchParams.set(key, String(value))
33
+ }
34
+
35
+ const response = await fetch(url.toString())
36
+ if (!response.ok) {
37
+ throw new Error(`CKAN API error: ${response.status} ${response.statusText}`)
38
+ }
39
+
40
+ const data = await response.json() as { success: boolean; result: T; error?: { message: string } }
41
+ if (!data.success) {
42
+ throw new Error(`CKAN error: ${data.error?.message ?? 'Unknown error'}`)
43
+ }
44
+
45
+ return data.result
46
+ }
47
+
48
+ export async function searchDatasets(query: string, limit: number = 10): Promise<CkanDataset[]> {
49
+ const result = await ckanAction<{ results: CkanDataset[] }>('package_search', { q: query, rows: limit })
50
+ return result.results
51
+ }
52
+
53
+ export async function getDataset(id: string): Promise<CkanDataset> {
54
+ return ckanAction<CkanDataset>('package_show', { id })
55
+ }
56
+
57
+ export async function getResourceData(
58
+ resourceId: string,
59
+ limit: number = 50,
60
+ offset: number = 0
61
+ ): Promise<CkanDatastoreResult> {
62
+ return ckanAction<CkanDatastoreResult>('datastore_search', {
63
+ resource_id: resourceId,
64
+ limit,
65
+ offset,
66
+ })
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { serve } from '@hono/node-server'
2
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'
3
+ import { createMcpHonoApp } from '@modelcontextprotocol/hono'
4
+ import type { Hono } from 'hono'
5
+ import { server } from './server.js'
6
+
7
+ const transport = new WebStandardStreamableHTTPServerTransport({
8
+ sessionIdGenerator: undefined,
9
+ })
10
+
11
+ await server.connect(transport)
12
+
13
+ type McpVars = { Variables: { parsedBody: unknown } }
14
+ const app = createMcpHonoApp() as unknown as Hono<McpVars>
15
+ app.all('/mcp', c => transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') }))
16
+
17
+ serve({ fetch: app.fetch, port: 3000 }, info => {
18
+ console.log(`MCP server running on http://localhost:${info.port}/mcp`)
19
+ })
package/src/server.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { McpServer } from '@modelcontextprotocol/server'
2
+ import { registerSearchTool } from './tools/search.js'
3
+ import { registerDatasetTool } from './tools/dataset.js'
4
+ import { registerResourceTool } from './tools/resource.js'
5
+
6
+ export const server = new McpServer({
7
+ name: 'datos-gob-cl',
8
+ version: '1.0.0',
9
+ })
10
+
11
+ registerSearchTool(server)
12
+ registerDatasetTool(server)
13
+ registerResourceTool(server)
package/src/stdio.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/server'
2
+ import { server } from './server.js'
3
+
4
+ const transport = new StdioServerTransport()
5
+ await server.connect(transport)
@@ -0,0 +1,48 @@
1
+ import type { McpServer } from '@modelcontextprotocol/server'
2
+ import { z } from 'zod'
3
+ import { getDataset } from '../ckan.js'
4
+
5
+ export function registerDatasetTool(server: McpServer): void {
6
+ server.registerTool(
7
+ 'get_dataset',
8
+ {
9
+ title: 'Get Dataset',
10
+ description: 'Get full metadata for a dataset from datos.gob.cl by its ID or slug. Use search_datasets first to find the ID.',
11
+ inputSchema: z.object({
12
+ id: z.string().describe('Dataset slug or UUID (e.g., "nombre-del-dataset" or a UUID from search results)'),
13
+ }),
14
+ },
15
+ async ({ id }) => {
16
+ try {
17
+ const dataset = await getDataset(id)
18
+ return {
19
+ content: [{
20
+ type: 'text',
21
+ text: JSON.stringify({
22
+ id: dataset.name,
23
+ title: dataset.title,
24
+ description: dataset.notes,
25
+ organization: dataset.organization?.title ?? null,
26
+ license: dataset.license_title,
27
+ tags: dataset.tags.map(t => t.name),
28
+ resources: dataset.resources.map(r => ({
29
+ id: r.id,
30
+ name: r.name,
31
+ format: r.format,
32
+ url: r.url,
33
+ datastore_available: r.datastore_active,
34
+ })),
35
+ }, null, 2),
36
+ }],
37
+ }
38
+ } catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error)
40
+ const isNotFound = message.toLowerCase().includes('not found')
41
+ return {
42
+ content: [{ type: 'text', text: isNotFound ? `Dataset not found: ${id}` : `Error: ${message}` }],
43
+ isError: true,
44
+ }
45
+ }
46
+ }
47
+ )
48
+ }
@@ -0,0 +1,47 @@
1
+ import type { McpServer } from '@modelcontextprotocol/server'
2
+ import { z } from 'zod'
3
+ import { getResourceData } from '../ckan.js'
4
+
5
+ export function registerResourceTool(server: McpServer): void {
6
+ server.registerTool(
7
+ 'get_resource_data',
8
+ {
9
+ title: 'Get Resource Data',
10
+ description: 'Read tabular data from a CKAN resource. Only works for resources with datastore enabled (check datastore_available from get_dataset). Supports pagination via limit and offset.',
11
+ inputSchema: z.object({
12
+ resource_id: z.string().describe('Resource UUID from a dataset\'s resources list (use get_dataset to obtain it)'),
13
+ limit: z.number().int().min(1).max(500).default(50).optional().describe('Rows to return (default: 50, max: 500)'),
14
+ offset: z.number().int().min(0).default(0).optional().describe('Row offset for pagination (default: 0)'),
15
+ }),
16
+ },
17
+ async ({ resource_id, limit, offset }) => {
18
+ try {
19
+ const result = await getResourceData(resource_id, limit ?? 50, offset ?? 0)
20
+ return {
21
+ content: [{
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ total: result.total,
25
+ returned: result.records.length,
26
+ offset: offset ?? 0,
27
+ fields: result.fields,
28
+ records: result.records,
29
+ }, null, 2),
30
+ }],
31
+ }
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error)
34
+ const isNoDatastore = message.toLowerCase().includes('datastore') || message.includes('404') || message.includes('NOT FOUND')
35
+ return {
36
+ content: [{
37
+ type: 'text',
38
+ text: isNoDatastore
39
+ ? `Datastore not available for resource "${resource_id}". This resource may be a file (CSV, XLS, PDF) without an activated datastore. Use the resource URL from get_dataset to download it directly.`
40
+ : `Error: ${message}`,
41
+ }],
42
+ isError: true,
43
+ }
44
+ }
45
+ }
46
+ )
47
+ }
@@ -0,0 +1,37 @@
1
+ import type { McpServer } from '@modelcontextprotocol/server'
2
+ import { z } from 'zod'
3
+ import { searchDatasets } from '../ckan.js'
4
+
5
+ export function registerSearchTool(server: McpServer): void {
6
+ server.registerTool(
7
+ 'search_datasets',
8
+ {
9
+ title: 'Search Datasets',
10
+ description: 'Search for datasets in the Chilean government open data portal (datos.gob.cl). Returns a list of matching datasets with their IDs, titles, and resource counts.',
11
+ inputSchema: z.object({
12
+ query: z.string().describe('Search query in Spanish or English (e.g., "educación", "salud", "transporte")'),
13
+ limit: z.number().int().min(1).max(100).default(10).optional().describe('Max number of results (default: 10, max: 100)'),
14
+ }),
15
+ },
16
+ async ({ query, limit }) => {
17
+ try {
18
+ const datasets = await searchDatasets(query, limit ?? 10)
19
+ const formatted = datasets.map(d => ({
20
+ id: d.name,
21
+ title: d.title,
22
+ description: d.notes?.slice(0, 200) ?? '',
23
+ organization: d.organization?.title ?? 'N/A',
24
+ resource_count: d.num_resources,
25
+ }))
26
+ return {
27
+ content: [{ type: 'text', text: JSON.stringify(formatted, null, 2) }],
28
+ }
29
+ } catch (error) {
30
+ return {
31
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
32
+ isError: true,
33
+ }
34
+ }
35
+ }
36
+ )
37
+ }