@aborruso/ckan-mcp-server 0.4.7 โ 0.4.9
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/AGENTS.md +188 -1
- package/CLAUDE.md +11 -8
- package/LOG.md +99 -0
- package/PRD.md +339 -252
- package/README.md +23 -17
- package/dist/index.js +87 -10
- package/dist/worker.js +240 -47
- package/package.json +2 -2
- package/testo.md +12 -0
- package/web-gui/PRD.md +158 -0
- package/web-gui/public/index.html +883 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ MCP (Model Context Protocol) server for interacting with CKAN-based open data po
|
|
|
14
14
|
- ๐จ Output in Markdown or JSON format
|
|
15
15
|
- โก Pagination and faceting support
|
|
16
16
|
- ๐ MCP Resource Templates for direct data access
|
|
17
|
-
- ๐งช
|
|
17
|
+
- ๐งช Test suite with 179 tests (100% passing)
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
@@ -36,7 +36,7 @@ npm install
|
|
|
36
36
|
# Build with esbuild (fast, ~4ms)
|
|
37
37
|
npm run build
|
|
38
38
|
|
|
39
|
-
# Run tests (
|
|
39
|
+
# Run tests (179 tests)
|
|
40
40
|
npm test
|
|
41
41
|
```
|
|
42
42
|
|
|
@@ -74,6 +74,8 @@ Use the public Workers endpoint (no local install required):
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
**NOTE**: This service uses the Cloudflare Workers free tier which has a limit of 100,000 requests per month.
|
|
78
|
+
|
|
77
79
|
Want your own deployment? See [DEPLOYMENT.md](docs/DEPLOYMENT.md).
|
|
78
80
|
|
|
79
81
|
### Claude Desktop Configuration
|
|
@@ -150,6 +152,8 @@ Use the public Cloudflare Workers deployment (no local installation required):
|
|
|
150
152
|
}
|
|
151
153
|
```
|
|
152
154
|
|
|
155
|
+
**NOTE**: This service uses the Cloudflare Workers free tier which has a limit of 100,000 requests per month.
|
|
156
|
+
|
|
153
157
|
**Note**: This uses the public endpoint. You can also deploy your own Workers instance and use that URL instead.
|
|
154
158
|
|
|
155
159
|
## Available Tools
|
|
@@ -200,7 +204,7 @@ ckan://data.gov/organization/sample-org
|
|
|
200
204
|
|
|
201
205
|
## Usage Examples
|
|
202
206
|
|
|
203
|
-
### Search datasets on dati.gov.it
|
|
207
|
+
### Search datasets on dati.gov.it (natural language: "search for population datasets")
|
|
204
208
|
|
|
205
209
|
```typescript
|
|
206
210
|
ckan_package_search({
|
|
@@ -210,7 +214,7 @@ ckan_package_search({
|
|
|
210
214
|
})
|
|
211
215
|
```
|
|
212
216
|
|
|
213
|
-
### Force text-field parser for long OR queries
|
|
217
|
+
### Force text-field parser for long OR queries (natural language: "find hotel or accommodation datasets")
|
|
214
218
|
|
|
215
219
|
```typescript
|
|
216
220
|
ckan_package_search({
|
|
@@ -221,7 +225,7 @@ ckan_package_search({
|
|
|
221
225
|
})
|
|
222
226
|
```
|
|
223
227
|
|
|
224
|
-
### Rank datasets by relevance
|
|
228
|
+
### Rank datasets by relevance (natural language: "find most relevant datasets about urban mobility")
|
|
225
229
|
|
|
226
230
|
```typescript
|
|
227
231
|
ckan_find_relevant_datasets({
|
|
@@ -231,7 +235,7 @@ ckan_find_relevant_datasets({
|
|
|
231
235
|
})
|
|
232
236
|
```
|
|
233
237
|
|
|
234
|
-
### Filter by organization
|
|
238
|
+
### Filter by organization (natural language: "show recent datasets from Sicilian Region")
|
|
235
239
|
|
|
236
240
|
```typescript
|
|
237
241
|
ckan_package_search({
|
|
@@ -241,7 +245,7 @@ ckan_package_search({
|
|
|
241
245
|
})
|
|
242
246
|
```
|
|
243
247
|
|
|
244
|
-
### Search organizations with wildcard
|
|
248
|
+
### Search organizations with wildcard (natural language: "find all organizations with health/salute in name")
|
|
245
249
|
|
|
246
250
|
```typescript
|
|
247
251
|
// Find all organizations containing "salute" in the name
|
|
@@ -254,7 +258,7 @@ ckan_package_search({
|
|
|
254
258
|
})
|
|
255
259
|
```
|
|
256
260
|
|
|
257
|
-
### Get statistics with faceting
|
|
261
|
+
### Get statistics with faceting (natural language: "show statistics by organization, tags and format")
|
|
258
262
|
|
|
259
263
|
```typescript
|
|
260
264
|
ckan_package_search({
|
|
@@ -283,7 +287,7 @@ ckan_group_search({
|
|
|
283
287
|
})
|
|
284
288
|
```
|
|
285
289
|
|
|
286
|
-
### DataStore Query
|
|
290
|
+
### DataStore Query (natural language: "query tabular data filtering by region and year")
|
|
287
291
|
|
|
288
292
|
```typescript
|
|
289
293
|
ckan_datastore_search({
|
|
@@ -295,7 +299,7 @@ ckan_datastore_search({
|
|
|
295
299
|
})
|
|
296
300
|
```
|
|
297
301
|
|
|
298
|
-
### DataStore SQL Query
|
|
302
|
+
### DataStore SQL Query (natural language: "count records by country with SQL")
|
|
299
303
|
|
|
300
304
|
```typescript
|
|
301
305
|
ckan_datastore_search_sql({
|
|
@@ -361,7 +365,7 @@ fq: "metadata_modified:[2023-01-01T00:00:00Z TO *]"
|
|
|
361
365
|
|
|
362
366
|
These real-world examples demonstrate powerful Solr query combinations tested on the Italian open data portal (dati.gov.it):
|
|
363
367
|
|
|
364
|
-
#### 1. Fuzzy Search + Date Math + Boosting
|
|
368
|
+
#### 1. Fuzzy Search + Date Math + Boosting (natural language: "find healthcare datasets modified in last 6 months")
|
|
365
369
|
|
|
366
370
|
Find healthcare datasets (tolerating spelling errors) modified in the last 6 months, prioritizing title matches:
|
|
367
371
|
|
|
@@ -383,7 +387,7 @@ ckan_package_search({
|
|
|
383
387
|
|
|
384
388
|
**Results**: 871 datasets including hospital units, healthcare organizations, medical services
|
|
385
389
|
|
|
386
|
-
#### 2. Proximity Search + Complex Boolean
|
|
390
|
+
#### 2. Proximity Search + Complex Boolean (natural language: "find air pollution datasets excluding water")
|
|
387
391
|
|
|
388
392
|
Environmental datasets where "inquinamento" and "aria" (air pollution) appear close together, excluding water-related datasets:
|
|
389
393
|
|
|
@@ -405,7 +409,7 @@ ckan_package_search({
|
|
|
405
409
|
|
|
406
410
|
**Results**: 306 datasets, primarily air quality monitoring from Milan (44) and Palermo (161), formats: XML (150), CSV (124), JSON (76)
|
|
407
411
|
|
|
408
|
-
#### 3. Wildcard + Field Existence + Range Queries
|
|
412
|
+
#### 3. Wildcard + Field Existence + Range Queries (natural language: "regional datasets with many resources from last year")
|
|
409
413
|
|
|
410
414
|
Regional datasets with at least 5 resources, published in the last year:
|
|
411
415
|
|
|
@@ -428,7 +432,7 @@ ckan_package_search({
|
|
|
428
432
|
|
|
429
433
|
**Results**: 5,318 datasets, top contributors: Lombardy (3,012), Tuscany (1,151), Puglia (460)
|
|
430
434
|
|
|
431
|
-
#### 4. Date Ranges + Exclusive Bounds
|
|
435
|
+
#### 4. Date Ranges + Exclusive Bounds (natural language: "ISTAT datasets with 10-50 resources from specific period")
|
|
432
436
|
|
|
433
437
|
ISTAT datasets with moderate resource count (10-50), modified in specific date range:
|
|
434
438
|
|
|
@@ -496,7 +500,7 @@ ckan-mcp-server/
|
|
|
496
500
|
โ โโโ transport/
|
|
497
501
|
โ โโโ stdio.ts # Stdio transport
|
|
498
502
|
โ โโโ http.ts # HTTP transport
|
|
499
|
-
โโโ tests/ # Test suite (
|
|
503
|
+
โโโ tests/ # Test suite (179 tests)
|
|
500
504
|
โโโ dist/ # Compiled files (generated)
|
|
501
505
|
โโโ package.json
|
|
502
506
|
โโโ README.md
|
|
@@ -519,12 +523,14 @@ npm run test:watch
|
|
|
519
523
|
npm run test:coverage
|
|
520
524
|
```
|
|
521
525
|
|
|
522
|
-
|
|
526
|
+
Current test coverage: ~39% (utils: 98%, tools: 15-20%).
|
|
523
527
|
|
|
524
|
-
|
|
528
|
+
Test suite includes:
|
|
529
|
+
- Unit tests for utility functions (formatting, HTTP, URI parsing, URL generation)
|
|
525
530
|
- Integration tests for MCP tools with mocked CKAN API responses
|
|
526
531
|
- Mock fixtures for CKAN API success and error scenarios
|
|
527
532
|
|
|
533
|
+
Coverage is higher for utility modules and lower for tool handlers.
|
|
528
534
|
See `tests/README.md` for detailed testing guidelines.
|
|
529
535
|
|
|
530
536
|
### Build
|
package/dist/index.js
CHANGED
|
@@ -411,6 +411,15 @@ ${params.fq ? `**Filter**: ${params.fq}
|
|
|
411
411
|
const sorted = Object.entries(facetValues).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
412
412
|
for (const [value, count] of sorted) {
|
|
413
413
|
markdown += `- **${value}**: ${count}
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
if (Object.keys(facetValues).length > sorted.length) {
|
|
417
|
+
markdown += `
|
|
418
|
+
Note: showing top ${sorted.length} only. Use \`response_format: json\` or increase \`facet_limit\`.
|
|
419
|
+
`;
|
|
420
|
+
} else {
|
|
421
|
+
markdown += `
|
|
422
|
+
Note: showing top ${sorted.length} only. Use \`response_format: json\` for full list.
|
|
414
423
|
`;
|
|
415
424
|
}
|
|
416
425
|
markdown += "\n";
|
|
@@ -897,16 +906,84 @@ Returns:
|
|
|
897
906
|
content: [{ type: "text", text: markdown2 }]
|
|
898
907
|
};
|
|
899
908
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
909
|
+
let result;
|
|
910
|
+
try {
|
|
911
|
+
result = await makeCkanRequest(
|
|
912
|
+
params.server_url,
|
|
913
|
+
"organization_list",
|
|
914
|
+
{
|
|
915
|
+
all_fields: params.all_fields,
|
|
916
|
+
sort: params.sort,
|
|
917
|
+
limit: params.limit,
|
|
918
|
+
offset: params.offset
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
923
|
+
if (message.includes("CKAN API error (500)")) {
|
|
924
|
+
const searchResult = await makeCkanRequest(
|
|
925
|
+
params.server_url,
|
|
926
|
+
"package_search",
|
|
927
|
+
{
|
|
928
|
+
rows: 0,
|
|
929
|
+
"facet.field": JSON.stringify(["organization"]),
|
|
930
|
+
"facet.limit": -1
|
|
931
|
+
}
|
|
932
|
+
);
|
|
933
|
+
const items = searchResult.search_facets?.organization?.items || [];
|
|
934
|
+
const sortValue = params.sort?.toLowerCase() ?? "name asc";
|
|
935
|
+
const sortedItems = [...items].sort((a, b) => {
|
|
936
|
+
if (sortValue.includes("package_count") || sortValue.includes("count")) {
|
|
937
|
+
return b.count - a.count;
|
|
938
|
+
}
|
|
939
|
+
if (sortValue.includes("name desc")) {
|
|
940
|
+
return String(b.name).localeCompare(String(a.name));
|
|
941
|
+
}
|
|
942
|
+
return String(a.name).localeCompare(String(b.name));
|
|
943
|
+
});
|
|
944
|
+
const pagedItems = sortedItems.slice(params.offset, params.offset + params.limit);
|
|
945
|
+
const organizations = pagedItems.map((item) => ({
|
|
946
|
+
id: item.name,
|
|
947
|
+
name: item.name,
|
|
948
|
+
title: item.display_name || item.name,
|
|
949
|
+
package_count: item.count
|
|
950
|
+
}));
|
|
951
|
+
if (params.response_format === "json" /* JSON */) {
|
|
952
|
+
const output = { count: items.length, organizations };
|
|
953
|
+
return {
|
|
954
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(output, null, 2)) }],
|
|
955
|
+
structuredContent: output
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
let markdown2 = `# CKAN Organizations
|
|
959
|
+
|
|
960
|
+
`;
|
|
961
|
+
markdown2 += `**Server**: ${params.server_url}
|
|
962
|
+
`;
|
|
963
|
+
markdown2 += `**Total**: ${items.length}
|
|
964
|
+
`;
|
|
965
|
+
markdown2 += `
|
|
966
|
+
Note: organization_list returned 500; using package_search facets.
|
|
967
|
+
|
|
968
|
+
`;
|
|
969
|
+
for (const org of organizations) {
|
|
970
|
+
markdown2 += `## ${org.title || org.name}
|
|
971
|
+
|
|
972
|
+
`;
|
|
973
|
+
markdown2 += `- **Name**: \`${org.name}\`
|
|
974
|
+
`;
|
|
975
|
+
markdown2 += `- **Datasets**: ${org.package_count || 0}
|
|
976
|
+
`;
|
|
977
|
+
markdown2 += `- **Link**: ${getOrganizationViewUrl(params.server_url, org)}
|
|
978
|
+
|
|
979
|
+
`;
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
content: [{ type: "text", text: truncateText(markdown2) }]
|
|
983
|
+
};
|
|
908
984
|
}
|
|
909
|
-
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
910
987
|
if (params.response_format === "json" /* JSON */) {
|
|
911
988
|
const output = Array.isArray(result) ? { count: result.length, organizations: result } : result;
|
|
912
989
|
return {
|
|
@@ -2149,7 +2226,7 @@ function registerAllResources(server2) {
|
|
|
2149
2226
|
function createServer() {
|
|
2150
2227
|
return new McpServer({
|
|
2151
2228
|
name: "ckan-mcp-server",
|
|
2152
|
-
version: "0.4.
|
|
2229
|
+
version: "0.4.8"
|
|
2153
2230
|
});
|
|
2154
2231
|
}
|
|
2155
2232
|
function registerAll(server2) {
|