@aborruso/ckan-mcp-server 0.4.14 โ†’ 0.4.16

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/EXAMPLES.md CHANGED
@@ -151,6 +151,24 @@ ckan_package_search({
151
151
  })
152
152
  ```
153
153
 
154
+ ### Get MQA quality metrics for a dataset
155
+ ```typescript
156
+ ckan_get_mqa_quality({
157
+ server_url: "https://www.dati.gov.it/opendata",
158
+ dataset_id: "332be8b7-89b9-4dfe-a252-7fccd3efda76",
159
+ response_format: "markdown"
160
+ })
161
+ ```
162
+
163
+ Returns quality score and detailed metrics from data.europa.eu MQA (Metadata Quality Assurance) system:
164
+ - Overall score (max 405 points)
165
+ - Accessibility (URL status, download availability)
166
+ - Reusability (license, contact point, publisher)
167
+ - Interoperability (format, media type)
168
+ - Findability (keywords, category, spatial/temporal coverage)
169
+
170
+ **Note**: Only works with dati.gov.it datasets. Uses the `identifier` field (or falls back to `name`) to query the European MQA API.
171
+
154
172
  ## USA Examples - data.gov
155
173
 
156
174
  ### Search government datasets
package/LOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # LOG
2
2
 
3
+ ## 2026-01-23
4
+
5
+ ### MQA Quality Metrics - Fix identifier format
6
+
7
+ - **Bug fix**: Identifier transformation for data.europa.eu API compatibility
8
+ - **Issue**: CKAN identifiers with colon separator (e.g., `c_f158:224c373e...`) were not recognized by MQA API
9
+ - **Root cause**: data.europa.eu uses hyphen-separated identifiers (`c_f158-224c373e...`)
10
+ - **Solution**: Replace colons with hyphens before API call: `.replace(/:/g, '-')`
11
+ - **Impact**: MQA quality metrics now work for all dati.gov.it datasets, including municipal portals
12
+ - **Example**: Messina air quality dataset now returns score 405/560 (Eccellente)
13
+ - **File modified**: `src/tools/quality.ts` (line 41)
14
+ - **Deployed**: Cloudflare Workers v0.4.16
15
+
16
+ ### MQA Quality Metrics Tool
17
+
18
+ - **Feature**: Added `ckan_get_mqa_quality` tool for retrieving quality metrics from data.europa.eu MQA API
19
+ - **Scope**: Only works with dati.gov.it datasets (server validation enforced)
20
+ - **Data source**: Queries https://data.europa.eu/api/mqa/cache/datasets/{identifier}
21
+ - **Identifier logic**: Uses `identifier` field from CKAN metadata, falls back to `name` if identifier is empty
22
+ - **Metrics returned**:
23
+ - Overall score (max 405 points)
24
+ - Accessibility (URL status, download availability)
25
+ - Reusability (license, contact point, publisher)
26
+ - Interoperability (format, media type)
27
+ - Findability (keywords, category, spatial/temporal coverage)
28
+ - **Output formats**: Markdown (default, human-readable) or JSON (structured data)
29
+ - **Error handling**: Dataset not found, MQA API unavailable, invalid server URL
30
+ - **Tests**: +11 integration tests (212 total, all passing)
31
+ - Server validation (www/non-www dati.gov.it URLs)
32
+ - Quality retrieval with identifier
33
+ - Fallback to name field
34
+ - Error scenarios (404, network errors)
35
+ - Markdown formatting (complete/partial data, availability indicators)
36
+ - **Documentation**: README.md (new Quality Metrics section), EXAMPLES.md (usage example with expected metrics)
37
+ - **Files**:
38
+ - `src/tools/quality.ts` (new, 194 lines)
39
+ - `src/server.ts` (register quality tools)
40
+ - `tests/integration/quality.test.ts` (new, 11 tests)
41
+ - `tests/fixtures/responses/mqa-quality-success.json` (new)
42
+ - `tests/fixtures/responses/package-show-{with,without}-identifier.json` (new)
43
+ - **OpenSpec**: Proposal in `openspec/changes/add-mqa-quality-tool/` (4 requirements, 11 scenarios)
44
+
3
45
  ## 2026-01-22
4
46
 
5
47
  ### Date Query Auto-Conversion (v0.4.14)
package/README.md CHANGED
@@ -17,7 +17,7 @@ MCP (Model Context Protocol) server for interacting with CKAN-based open data po
17
17
  - โšก Pagination and faceting support
18
18
  - ๐Ÿ“„ MCP Resource Templates for direct data access
19
19
  - ๐Ÿงญ Guided MCP prompts for common workflows
20
- - ๐Ÿงช Test suite with 191 tests (100% passing)
20
+ - ๐Ÿงช Test suite with 212 tests (100% passing)
21
21
 
22
22
  ---
23
23
 
@@ -45,7 +45,7 @@ npm install
45
45
  # Build with esbuild (fast, ~4ms)
46
46
  npm run build
47
47
 
48
- # Run tests (191 tests)
48
+ # Run tests (212 tests)
49
49
  npm test
50
50
  ```
51
51
 
@@ -209,6 +209,10 @@ These guides are based on a public demo server, which has a limit of 100,000 cal
209
209
  - **ckan_group_show**: Show group details
210
210
  - **ckan_group_search**: Search groups by name
211
211
 
212
+ ### Quality Metrics
213
+
214
+ - **ckan_get_mqa_quality**: Get MQA quality score and metrics for dati.gov.it datasets (accessibility, reusability, interoperability, findability)
215
+
212
216
  ### Utilities
213
217
 
214
218
  - **ckan_status_show**: Verify server status
@@ -566,7 +570,7 @@ ckan-mcp-server/
566
570
  โ”‚ โ””โ”€โ”€ transport/
567
571
  โ”‚ โ”œโ”€โ”€ stdio.ts # Stdio transport
568
572
  โ”‚ โ””โ”€โ”€ http.ts # HTTP transport
569
- โ”œโ”€โ”€ tests/ # Test suite (191 tests)
573
+ โ”œโ”€โ”€ tests/ # Test suite (212 tests)
570
574
  โ”œโ”€โ”€ dist/ # Compiled files (generated)
571
575
  โ”œโ”€โ”€ package.json
572
576
  โ””โ”€โ”€ README.md
package/dist/index.js CHANGED
@@ -2101,6 +2101,146 @@ Returns:
2101
2101
  );
2102
2102
  }
2103
2103
 
2104
+ // src/tools/quality.ts
2105
+ import { z as z8 } from "zod";
2106
+ import axios2 from "axios";
2107
+ var MQA_API_BASE = "https://data.europa.eu/api/mqa/cache/datasets";
2108
+ var ALLOWED_SERVER_PATTERNS = [
2109
+ /^https?:\/\/(www\.)?dati\.gov\.it/i
2110
+ ];
2111
+ function isValidMqaServer(serverUrl) {
2112
+ return ALLOWED_SERVER_PATTERNS.some((pattern) => pattern.test(serverUrl));
2113
+ }
2114
+ async function getMqaQuality(serverUrl, datasetId) {
2115
+ const dataset = await makeCkanRequest(
2116
+ serverUrl,
2117
+ "package_show",
2118
+ { id: datasetId }
2119
+ );
2120
+ const europeanId = (dataset.identifier || dataset.name).replace(/:/g, "-");
2121
+ const mqaUrl = `${MQA_API_BASE}/${europeanId}`;
2122
+ try {
2123
+ const response = await axios2.get(mqaUrl, {
2124
+ timeout: 3e4,
2125
+ headers: {
2126
+ "User-Agent": "CKAN-MCP-Server/1.0"
2127
+ }
2128
+ });
2129
+ return response.data;
2130
+ } catch (error) {
2131
+ if (axios2.isAxiosError(error)) {
2132
+ if (error.response?.status === 404) {
2133
+ throw new Error(`Quality metrics not found for dataset '${europeanId}' on data.europa.eu`);
2134
+ }
2135
+ throw new Error(`MQA API error: ${error.message}`);
2136
+ }
2137
+ throw error;
2138
+ }
2139
+ }
2140
+ function formatQualityMarkdown(data, datasetId) {
2141
+ const lines = [];
2142
+ lines.push(`# Quality Metrics for Dataset: ${datasetId}`);
2143
+ lines.push("");
2144
+ if (data.info?.score !== void 0) {
2145
+ lines.push(`**Overall Score**: ${data.info.score}/405`);
2146
+ lines.push("");
2147
+ }
2148
+ if (data.accessibility) {
2149
+ lines.push("## Accessibility");
2150
+ if (data.accessibility.accessUrl !== void 0) {
2151
+ lines.push(`- Access URL: ${data.accessibility.accessUrl.available ? "\u2713" : "\u2717"} Available`);
2152
+ }
2153
+ if (data.accessibility.downloadUrl !== void 0) {
2154
+ lines.push(`- Download URL: ${data.accessibility.downloadUrl.available ? "\u2713" : "\u2717"} Available`);
2155
+ }
2156
+ lines.push("");
2157
+ }
2158
+ if (data.reusability) {
2159
+ lines.push("## Reusability");
2160
+ if (data.reusability.licence !== void 0) {
2161
+ lines.push(`- License: ${data.reusability.licence.available ? "\u2713" : "\u2717"} Available`);
2162
+ }
2163
+ if (data.reusability.contactPoint !== void 0) {
2164
+ lines.push(`- Contact Point: ${data.reusability.contactPoint.available ? "\u2713" : "\u2717"} Available`);
2165
+ }
2166
+ if (data.reusability.publisher !== void 0) {
2167
+ lines.push(`- Publisher: ${data.reusability.publisher.available ? "\u2713" : "\u2717"} Available`);
2168
+ }
2169
+ lines.push("");
2170
+ }
2171
+ if (data.interoperability) {
2172
+ lines.push("## Interoperability");
2173
+ if (data.interoperability.format !== void 0) {
2174
+ lines.push(`- Format: ${data.interoperability.format.available ? "\u2713" : "\u2717"} Available`);
2175
+ }
2176
+ if (data.interoperability.mediaType !== void 0) {
2177
+ lines.push(`- Media Type: ${data.interoperability.mediaType.available ? "\u2713" : "\u2717"} Available`);
2178
+ }
2179
+ lines.push("");
2180
+ }
2181
+ if (data.findability) {
2182
+ lines.push("## Findability");
2183
+ if (data.findability.keyword !== void 0) {
2184
+ lines.push(`- Keywords: ${data.findability.keyword.available ? "\u2713" : "\u2717"} Available`);
2185
+ }
2186
+ if (data.findability.category !== void 0) {
2187
+ lines.push(`- Category: ${data.findability.category.available ? "\u2713" : "\u2717"} Available`);
2188
+ }
2189
+ if (data.findability.spatial !== void 0) {
2190
+ lines.push(`- Spatial: ${data.findability.spatial.available ? "\u2713" : "\u2717"} Available`);
2191
+ }
2192
+ if (data.findability.temporal !== void 0) {
2193
+ lines.push(`- Temporal: ${data.findability.temporal.available ? "\u2713" : "\u2717"} Available`);
2194
+ }
2195
+ lines.push("");
2196
+ }
2197
+ lines.push("---");
2198
+ lines.push(`Source: ${MQA_API_BASE}/${data.id || datasetId}`);
2199
+ return lines.join("\n");
2200
+ }
2201
+ function registerQualityTools(server2) {
2202
+ server2.tool(
2203
+ "ckan_get_mqa_quality",
2204
+ "Get MQA (Metadata Quality Assurance) quality metrics for a dataset on dati.gov.it. Returns quality score and detailed metrics (accessibility, reusability, interoperability, findability) from data.europa.eu. Only works with dati.gov.it server.",
2205
+ {
2206
+ server_url: z8.string().url().describe("Base URL of dati.gov.it (e.g., https://www.dati.gov.it/opendata)"),
2207
+ dataset_id: z8.string().describe("Dataset ID or name"),
2208
+ response_format: ResponseFormatSchema.optional()
2209
+ },
2210
+ async ({ server_url, dataset_id, response_format }) => {
2211
+ if (!isValidMqaServer(server_url)) {
2212
+ return {
2213
+ content: [{
2214
+ type: "text",
2215
+ text: `Error: MQA quality metrics are only available for dati.gov.it datasets. Provided server: ${server_url}
2216
+
2217
+ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and only evaluates datasets from Italian open data portal.`
2218
+ }]
2219
+ };
2220
+ }
2221
+ try {
2222
+ const qualityData = await getMqaQuality(server_url, dataset_id);
2223
+ const format = response_format || "markdown" /* MARKDOWN */;
2224
+ const output = format === "json" /* JSON */ ? JSON.stringify(qualityData, null, 2) : formatQualityMarkdown(qualityData, dataset_id);
2225
+ return {
2226
+ content: [{
2227
+ type: "text",
2228
+ text: output
2229
+ }]
2230
+ };
2231
+ } catch (error) {
2232
+ const errorMessage = error instanceof Error ? error.message : String(error);
2233
+ return {
2234
+ content: [{
2235
+ type: "text",
2236
+ text: `Error retrieving quality metrics: ${errorMessage}`
2237
+ }]
2238
+ };
2239
+ }
2240
+ }
2241
+ );
2242
+ }
2243
+
2104
2244
  // src/resources/dataset.ts
2105
2245
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2106
2246
 
@@ -2370,7 +2510,7 @@ function registerAllResources(server2) {
2370
2510
  }
2371
2511
 
2372
2512
  // src/prompts/theme.ts
2373
- import { z as z8 } from "zod";
2513
+ import { z as z9 } from "zod";
2374
2514
 
2375
2515
  // src/prompts/types.ts
2376
2516
  var createTextPrompt = (text) => ({
@@ -2427,9 +2567,9 @@ var registerThemePrompt = (server2) => {
2427
2567
  title: "Search datasets by theme",
2428
2568
  description: "Guided prompt to discover a theme and search datasets under it.",
2429
2569
  argsSchema: {
2430
- server_url: z8.string().url().describe("Base URL of the CKAN server"),
2431
- theme: z8.string().min(1).describe("Theme or group name to search"),
2432
- rows: z8.coerce.number().int().positive().default(10).describe("Max results to return")
2570
+ server_url: z9.string().url().describe("Base URL of the CKAN server"),
2571
+ theme: z9.string().min(1).describe("Theme or group name to search"),
2572
+ rows: z9.coerce.number().int().positive().default(10).describe("Max results to return")
2433
2573
  }
2434
2574
  },
2435
2575
  async ({ server_url, theme, rows }) => createTextPrompt(buildThemePromptText(server_url, theme, rows))
@@ -2437,7 +2577,7 @@ var registerThemePrompt = (server2) => {
2437
2577
  };
2438
2578
 
2439
2579
  // src/prompts/organization.ts
2440
- import { z as z9 } from "zod";
2580
+ import { z as z10 } from "zod";
2441
2581
  var ORGANIZATION_PROMPT_NAME = "ckan-search-by-organization";
2442
2582
  var buildOrganizationPromptText = (serverUrl, organization, rows) => `# Guided search: datasets by organization
2443
2583
 
@@ -2468,9 +2608,9 @@ var registerOrganizationPrompt = (server2) => {
2468
2608
  title: "Search datasets by organization",
2469
2609
  description: "Guided prompt to find a publisher and list its datasets.",
2470
2610
  argsSchema: {
2471
- server_url: z9.string().url().describe("Base URL of the CKAN server"),
2472
- organization: z9.string().min(1).describe("Organization name or keyword"),
2473
- rows: z9.coerce.number().int().positive().default(10).describe("Max results to return")
2611
+ server_url: z10.string().url().describe("Base URL of the CKAN server"),
2612
+ organization: z10.string().min(1).describe("Organization name or keyword"),
2613
+ rows: z10.coerce.number().int().positive().default(10).describe("Max results to return")
2474
2614
  }
2475
2615
  },
2476
2616
  async ({ server_url, organization, rows }) => createTextPrompt(buildOrganizationPromptText(server_url, organization, rows))
@@ -2478,7 +2618,7 @@ var registerOrganizationPrompt = (server2) => {
2478
2618
  };
2479
2619
 
2480
2620
  // src/prompts/format.ts
2481
- import { z as z10 } from "zod";
2621
+ import { z as z11 } from "zod";
2482
2622
  var FORMAT_PROMPT_NAME = "ckan-search-by-format";
2483
2623
  var buildFormatPromptText = (serverUrl, format, rows) => `# Guided search: datasets by resource format
2484
2624
 
@@ -2499,9 +2639,9 @@ var registerFormatPrompt = (server2) => {
2499
2639
  title: "Search datasets by resource format",
2500
2640
  description: "Guided prompt to find datasets with a given resource format.",
2501
2641
  argsSchema: {
2502
- server_url: z10.string().url().describe("Base URL of the CKAN server"),
2503
- format: z10.string().min(1).describe("Resource format (e.g., CSV, JSON)"),
2504
- rows: z10.coerce.number().int().positive().default(10).describe("Max results to return")
2642
+ server_url: z11.string().url().describe("Base URL of the CKAN server"),
2643
+ format: z11.string().min(1).describe("Resource format (e.g., CSV, JSON)"),
2644
+ rows: z11.coerce.number().int().positive().default(10).describe("Max results to return")
2505
2645
  }
2506
2646
  },
2507
2647
  async ({ server_url, format, rows }) => createTextPrompt(buildFormatPromptText(server_url, format, rows))
@@ -2509,7 +2649,7 @@ var registerFormatPrompt = (server2) => {
2509
2649
  };
2510
2650
 
2511
2651
  // src/prompts/recent.ts
2512
- import { z as z11 } from "zod";
2652
+ import { z as z12 } from "zod";
2513
2653
  var RECENT_PROMPT_NAME = "ckan-recent-datasets";
2514
2654
  var buildRecentPromptText = (serverUrl, rows) => `# Guided search: recent datasets
2515
2655
 
@@ -2537,8 +2677,8 @@ var registerRecentPrompt = (server2) => {
2537
2677
  title: "Find recently updated datasets",
2538
2678
  description: "Guided prompt to list recently updated datasets on a CKAN portal.",
2539
2679
  argsSchema: {
2540
- server_url: z11.string().url().describe("Base URL of the CKAN server"),
2541
- rows: z11.coerce.number().int().positive().default(10).describe("Max results to return")
2680
+ server_url: z12.string().url().describe("Base URL of the CKAN server"),
2681
+ rows: z12.coerce.number().int().positive().default(10).describe("Max results to return")
2542
2682
  }
2543
2683
  },
2544
2684
  async ({ server_url, rows }) => createTextPrompt(buildRecentPromptText(server_url, rows))
@@ -2546,7 +2686,7 @@ var registerRecentPrompt = (server2) => {
2546
2686
  };
2547
2687
 
2548
2688
  // src/prompts/dataset-analysis.ts
2549
- import { z as z12 } from "zod";
2689
+ import { z as z13 } from "zod";
2550
2690
  var DATASET_ANALYSIS_PROMPT_NAME = "ckan-analyze-dataset";
2551
2691
  var buildDatasetAnalysisPromptText = (serverUrl, id) => `# Guided analysis: dataset
2552
2692
 
@@ -2588,8 +2728,8 @@ var registerDatasetAnalysisPrompt = (server2) => {
2588
2728
  title: "Analyze a dataset",
2589
2729
  description: "Guided prompt to inspect dataset metadata and explore DataStore tables.",
2590
2730
  argsSchema: {
2591
- server_url: z12.string().url().describe("Base URL of the CKAN server"),
2592
- id: z12.string().min(1).describe("Dataset id or name (CKAN package id)")
2731
+ server_url: z13.string().url().describe("Base URL of the CKAN server"),
2732
+ id: z13.string().min(1).describe("Dataset id or name (CKAN package id)")
2593
2733
  }
2594
2734
  },
2595
2735
  async ({ server_url, id }) => createTextPrompt(buildDatasetAnalysisPromptText(server_url, id))
@@ -2609,7 +2749,7 @@ var registerAllPrompts = (server2) => {
2609
2749
  function createServer() {
2610
2750
  return new McpServer({
2611
2751
  name: "ckan-mcp-server",
2612
- version: "0.4.13"
2752
+ version: "0.4.16"
2613
2753
  });
2614
2754
  }
2615
2755
  function registerAll(server2) {
@@ -2619,6 +2759,7 @@ function registerAll(server2) {
2619
2759
  registerStatusTools(server2);
2620
2760
  registerTagTools(server2);
2621
2761
  registerGroupTools(server2);
2762
+ registerQualityTools(server2);
2622
2763
  registerAllResources(server2);
2623
2764
  registerAllPrompts(server2);
2624
2765
  }