@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/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
- - ๐Ÿงช Comprehensive test suite (120 tests, 100% passing)
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 (120 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 (120 tests)
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
- Test coverage target is 80%. Current test suite includes:
526
+ Current test coverage: ~39% (utils: 98%, tools: 15-20%).
523
527
 
524
- - Unit tests for utility functions (formatting, HTTP)
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
- const result = await makeCkanRequest(
901
- params.server_url,
902
- "organization_list",
903
- {
904
- all_fields: params.all_fields,
905
- sort: params.sort,
906
- limit: params.limit,
907
- offset: params.offset
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.7"
2229
+ version: "0.4.8"
2153
2230
  });
2154
2231
  }
2155
2232
  function registerAll(server2) {