@aborruso/ckan-mcp-server 0.4.33 → 0.4.36

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/LOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## 2026-02-01
4
4
 
5
+ ### Release v0.4.36
6
+
7
+ - Fix: align server and worker reported version with package version
8
+ - Fix: update worker health tool count
9
+ - Files: `src/server.ts`, `src/worker.ts`
10
+
11
+ ### Unreleased
12
+
13
+ - None
14
+
15
+ ### Release v0.4.35
16
+
17
+ - Tests: adjust MQA metrics details fixture to include scoring entries
18
+ - Files: `tests/integration/quality.test.ts`
19
+
20
+ ### Release v0.4.34
21
+
22
+ - MQA: add detailed quality reasons tool with metrics flag parsing
23
+ - MQA: add guidance note to use metrics endpoint for score deductions
24
+ - Tests: cover detailed MQA reasons output and guidance note
25
+ - Docs: list `ckan_get_mqa_quality_details` tool
26
+ - Files: `src/tools/quality.ts`, `tests/integration/quality.test.ts`, `README.md`, `docs/architecture-flow.md`
27
+
28
+ ### Unreleased
29
+
30
+ - None
31
+
5
32
  ### Release v0.4.33
6
33
 
7
34
  - Docs: clarify natural language date-field mapping for package search and document `content_recent` usage with example
package/README.md CHANGED
@@ -260,6 +260,7 @@ See [Claude web guide](docs/guide/claude/claude_web.md)
260
260
  ### Quality Metrics
261
261
 
262
262
  - **ckan_get_mqa_quality**: Get MQA quality score and metrics for dati.gov.it datasets (accessibility, reusability, interoperability, findability)
263
+ - **ckan_get_mqa_quality_details**: Get detailed MQA quality reasons and failing flags for dati.gov.it datasets
263
264
 
264
265
  ### Utilities
265
266
 
package/dist/index.js CHANGED
@@ -2458,6 +2458,47 @@ var DIMENSION_MAX = {
2458
2458
  reusability: 75,
2459
2459
  contextuality: 20
2460
2460
  };
2461
+ var DIMENSION_LABELS = {
2462
+ accessibility: "Accessibility",
2463
+ findability: "Findability",
2464
+ interoperability: "Interoperability",
2465
+ reusability: "Reusability",
2466
+ contextuality: "Contextuality"
2467
+ };
2468
+ var METRIC_DEFINITIONS = {
2469
+ accessUrlAvailability: { dimension: "accessibility", reason: "accessUrlAvailability=false" },
2470
+ downloadUrlAvailability: { dimension: "accessibility", reason: "downloadUrlAvailability=false" },
2471
+ accessUrlStatusCode: { dimension: "accessibility", expectsStatusCode: true },
2472
+ downloadUrlStatusCode: { dimension: "accessibility", expectsStatusCode: true },
2473
+ keywordAvailability: { dimension: "findability", reason: "keywordAvailability=false" },
2474
+ categoryAvailability: { dimension: "findability", reason: "categoryAvailability=false" },
2475
+ spatialAvailability: { dimension: "findability", reason: "spatialAvailability=false" },
2476
+ temporalAvailability: { dimension: "findability", reason: "temporalAvailability=false" },
2477
+ dcatApCompliance: { dimension: "interoperability", reason: "dcatApCompliance=false" },
2478
+ formatAvailability: { dimension: "interoperability", reason: "formatAvailability=false" },
2479
+ mediaTypeAvailability: { dimension: "interoperability", reason: "mediaTypeAvailability=false" },
2480
+ formatMediaTypeVocabularyAlignment: { dimension: "interoperability", reason: "formatMediaTypeVocabularyAlignment=false" },
2481
+ formatMediaTypeMachineInterpretable: {
2482
+ dimension: "interoperability",
2483
+ reason: "formatMediaTypeMachineInterpretable=false"
2484
+ },
2485
+ accessRightsAvailability: { dimension: "reusability", reason: "accessRightsAvailability=false" },
2486
+ accessRightsVocabularyAlignment: {
2487
+ dimension: "reusability",
2488
+ reason: "accessRightsVocabularyAlignment=false"
2489
+ },
2490
+ licenceAvailability: { dimension: "reusability", reason: "licenceAvailability=false" },
2491
+ knownLicence: {
2492
+ dimension: "reusability",
2493
+ reason: "knownLicence=false (licence not aligned to controlled vocabulary)"
2494
+ },
2495
+ contactPointAvailability: { dimension: "reusability", reason: "contactPointAvailability=false" },
2496
+ publisherAvailability: { dimension: "reusability", reason: "publisherAvailability=false" },
2497
+ byteSizeAvailability: { dimension: "contextuality", reason: "byteSizeAvailability=false" },
2498
+ rightsAvailability: { dimension: "contextuality", reason: "rightsAvailability=false" },
2499
+ dateModifiedAvailability: { dimension: "contextuality", reason: "dateModifiedAvailability=false" },
2500
+ dateIssuedAvailability: { dimension: "contextuality", reason: "dateIssuedAvailability=false" }
2501
+ };
2461
2502
  function parseScoreValue(value) {
2462
2503
  if (!value || typeof value !== "object") {
2463
2504
  return void 0;
@@ -2472,6 +2513,44 @@ function parseScoreValue(value) {
2472
2513
  }
2473
2514
  return void 0;
2474
2515
  }
2516
+ function parseMetricValue(value) {
2517
+ if (!value || typeof value !== "object") {
2518
+ if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2519
+ return value;
2520
+ }
2521
+ return void 0;
2522
+ }
2523
+ const raw = value["@value"];
2524
+ if (typeof raw === "boolean") {
2525
+ return raw;
2526
+ }
2527
+ if (typeof raw === "number") {
2528
+ return raw;
2529
+ }
2530
+ if (typeof raw === "string") {
2531
+ const lower = raw.toLowerCase();
2532
+ if (lower === "true" || lower === "false") {
2533
+ return lower === "true";
2534
+ }
2535
+ const numeric = Number(raw);
2536
+ if (Number.isFinite(numeric)) {
2537
+ return numeric;
2538
+ }
2539
+ return raw;
2540
+ }
2541
+ return void 0;
2542
+ }
2543
+ function metricKeyFromId(metricId) {
2544
+ const hashIndex = metricId.lastIndexOf("#");
2545
+ if (hashIndex >= 0 && hashIndex < metricId.length - 1) {
2546
+ return metricId.slice(hashIndex + 1);
2547
+ }
2548
+ const slashIndex = metricId.lastIndexOf("/");
2549
+ if (slashIndex >= 0 && slashIndex < metricId.length - 1) {
2550
+ return metricId.slice(slashIndex + 1);
2551
+ }
2552
+ return metricId;
2553
+ }
2475
2554
  function decodeMetricsPayload(payload) {
2476
2555
  if (payload && typeof payload === "object" && "@graph" in payload) {
2477
2556
  return payload;
@@ -2544,6 +2623,86 @@ function extractMetricsScores(metricsData) {
2544
2623
  }
2545
2624
  return scores;
2546
2625
  }
2626
+ function extractMetricDetails(metricsData, nonMaxDimensions) {
2627
+ const parsed = decodeMetricsPayload(metricsData);
2628
+ if (!parsed || typeof parsed !== "object") {
2629
+ return { flags: [], reasons: {} };
2630
+ }
2631
+ const graph = parsed["@graph"];
2632
+ if (!Array.isArray(graph)) {
2633
+ return { flags: [], reasons: {} };
2634
+ }
2635
+ const flagsMap = /* @__PURE__ */ new Map();
2636
+ for (const node of graph) {
2637
+ if (!node || typeof node !== "object") {
2638
+ continue;
2639
+ }
2640
+ const metricRef = node["dqv:isMeasurementOf"];
2641
+ if (!metricRef) {
2642
+ continue;
2643
+ }
2644
+ const metricId = typeof metricRef === "string" ? metricRef : metricRef["@id"];
2645
+ if (typeof metricId !== "string") {
2646
+ continue;
2647
+ }
2648
+ const metricKey = metricKeyFromId(metricId);
2649
+ const definition = METRIC_DEFINITIONS[metricKey];
2650
+ if (!definition) {
2651
+ continue;
2652
+ }
2653
+ const value = parseMetricValue(node["dqv:value"]);
2654
+ if (value === void 0) {
2655
+ continue;
2656
+ }
2657
+ const entry = flagsMap.get(metricKey);
2658
+ if (entry) {
2659
+ entry.values.add(value);
2660
+ continue;
2661
+ }
2662
+ flagsMap.set(metricKey, {
2663
+ metricId,
2664
+ metricKey,
2665
+ dimension: definition.dimension,
2666
+ values: /* @__PURE__ */ new Set([value])
2667
+ });
2668
+ }
2669
+ const reasons = {};
2670
+ const reasonSets = /* @__PURE__ */ new Map();
2671
+ for (const dimension of nonMaxDimensions) {
2672
+ reasonSets.set(dimension, /* @__PURE__ */ new Set());
2673
+ }
2674
+ for (const entry of flagsMap.values()) {
2675
+ const definition = METRIC_DEFINITIONS[entry.metricKey];
2676
+ if (!definition) {
2677
+ continue;
2678
+ }
2679
+ const reasonSet = reasonSets.get(entry.dimension);
2680
+ if (!reasonSet) {
2681
+ continue;
2682
+ }
2683
+ for (const value of entry.values) {
2684
+ if (typeof value === "boolean" && value === false) {
2685
+ reasonSet.add(definition.reason || `${entry.metricKey}=false`);
2686
+ continue;
2687
+ }
2688
+ if (definition.expectsStatusCode && typeof value === "number" && value !== 200) {
2689
+ reasonSet.add(`${entry.metricKey}=${value}`);
2690
+ }
2691
+ }
2692
+ }
2693
+ for (const [dimension, set] of reasonSets.entries()) {
2694
+ if (set.size > 0) {
2695
+ reasons[dimension] = Array.from(set.values());
2696
+ }
2697
+ }
2698
+ const flags = Array.from(flagsMap.values()).map((entry) => ({
2699
+ metricId: entry.metricId,
2700
+ metricKey: entry.metricKey,
2701
+ dimension: entry.dimension,
2702
+ values: Array.from(entry.values)
2703
+ }));
2704
+ return { flags, reasons };
2705
+ }
2547
2706
  function findNonMaxDimensions(scores) {
2548
2707
  const nonMax = [];
2549
2708
  Object.keys(DIMENSION_MAX).forEach((dimension) => {
@@ -2554,7 +2713,7 @@ function findNonMaxDimensions(scores) {
2554
2713
  });
2555
2714
  return nonMax;
2556
2715
  }
2557
- async function getMqaQuality(serverUrl, datasetId) {
2716
+ async function fetchMqaQuality(serverUrl, datasetId) {
2558
2717
  const dataset = await makeCkanRequest(
2559
2718
  serverUrl,
2560
2719
  "package_show",
@@ -2608,6 +2767,7 @@ async function getMqaQuality(serverUrl, datasetId) {
2608
2767
  };
2609
2768
  return {
2610
2769
  mqa: response.data,
2770
+ metrics: metricsPayload,
2611
2771
  breakdown
2612
2772
  };
2613
2773
  } catch (error) {
@@ -2624,6 +2784,21 @@ async function getMqaQuality(serverUrl, datasetId) {
2624
2784
  `Quality metrics not found or identifier not aligned on data.europa.eu. Tried: ${candidates.join(", ")}. Check the dataset quality page on data.europa.eu to confirm the identifier (it may include a '~~1' suffix) or verify alignment on dati.gov.it (quality may be marked as 'Non disponibile o identificativo non allineato').`
2625
2785
  );
2626
2786
  }
2787
+ async function getMqaQuality(serverUrl, datasetId) {
2788
+ const result = await fetchMqaQuality(serverUrl, datasetId);
2789
+ return {
2790
+ mqa: result.mqa,
2791
+ breakdown: result.breakdown
2792
+ };
2793
+ }
2794
+ async function getMqaQualityDetails(serverUrl, datasetId) {
2795
+ const result = await fetchMqaQuality(serverUrl, datasetId);
2796
+ const details = extractMetricDetails(result.metrics, result.breakdown.nonMaxDimensions);
2797
+ return {
2798
+ breakdown: result.breakdown,
2799
+ details
2800
+ };
2801
+ }
2627
2802
  function findSectionMetric(section, key) {
2628
2803
  if (!Array.isArray(section)) {
2629
2804
  return void 0;
@@ -2818,7 +2993,57 @@ function formatQualityMarkdown(data, datasetId) {
2818
2993
  const portalId = normalized.breakdown?.portalId || normalized.id || datasetId;
2819
2994
  lines.push(`Portal: https://data.europa.eu/data/datasets/${portalId}/quality?locale=it`);
2820
2995
  lines.push(`MQA source: ${MQA_API_BASE}/${portalId}`);
2821
- lines.push(`Metrics endpoint: ${normalized.breakdown?.metricsUrl || `${MQA_METRICS_BASE}/${portalId}/metrics`}`);
2996
+ const metricsEndpoint = normalized.breakdown?.metricsUrl || `${MQA_METRICS_BASE}/${portalId}/metrics`;
2997
+ lines.push(`Metrics endpoint: ${metricsEndpoint}`);
2998
+ lines.push(`Tip: Use the metrics endpoint to explain score deductions (e.g., failing measurements such as knownLicence = false).`);
2999
+ return lines.join("\n");
3000
+ }
3001
+ function formatQualityDetailsMarkdown(data, datasetId) {
3002
+ const lines = [];
3003
+ const breakdown = data.breakdown;
3004
+ lines.push(`# Quality Details for Dataset: ${datasetId}`);
3005
+ lines.push("");
3006
+ if (typeof breakdown.scores.total === "number") {
3007
+ lines.push(`**Overall Score**: ${breakdown.scores.total}/405`);
3008
+ lines.push("");
3009
+ }
3010
+ if (breakdown.scores) {
3011
+ lines.push("## Dimension Scores");
3012
+ const order = [
3013
+ ["accessibility", DIMENSION_MAX.accessibility],
3014
+ ["findability", DIMENSION_MAX.findability],
3015
+ ["interoperability", DIMENSION_MAX.interoperability],
3016
+ ["reusability", DIMENSION_MAX.reusability],
3017
+ ["contextuality", DIMENSION_MAX.contextuality]
3018
+ ];
3019
+ for (const [key, max] of order) {
3020
+ const value = breakdown.scores[key];
3021
+ if (typeof value === "number") {
3022
+ const status = value >= max ? "\u2705" : "\u26A0\uFE0F";
3023
+ lines.push(`- ${DIMENSION_LABELS[key]}: ${value}/${max} ${status}${value >= max ? "" : ` (max ${max})`}`);
3024
+ }
3025
+ }
3026
+ lines.push("");
3027
+ }
3028
+ lines.push("## Non-max Reasons");
3029
+ if (breakdown.nonMaxDimensions.length === 0) {
3030
+ lines.push("- All dimensions are at max score.");
3031
+ } else {
3032
+ for (const dimension of breakdown.nonMaxDimensions) {
3033
+ const reasons = data.details.reasons[dimension] || [];
3034
+ if (reasons.length === 0) {
3035
+ lines.push(`- ${DIMENSION_LABELS[dimension]}: no failing flags detected in metrics payload`);
3036
+ } else {
3037
+ lines.push(`- ${DIMENSION_LABELS[dimension]}: ${reasons.join("; ")}`);
3038
+ }
3039
+ }
3040
+ }
3041
+ lines.push("");
3042
+ lines.push("---");
3043
+ const portalId = breakdown.portalId || datasetId;
3044
+ lines.push(`Portal: https://data.europa.eu/data/datasets/${portalId}/quality?locale=it`);
3045
+ lines.push(`MQA source: ${MQA_API_BASE}/${portalId}`);
3046
+ lines.push(`Metrics endpoint: ${breakdown.metricsUrl || `${MQA_METRICS_BASE}/${portalId}/metrics`}`);
2822
3047
  return lines.join("\n");
2823
3048
  }
2824
3049
  function registerQualityTools(server2) {
@@ -2862,6 +3087,46 @@ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and on
2862
3087
  }
2863
3088
  }
2864
3089
  );
3090
+ server2.tool(
3091
+ "ckan_get_mqa_quality_details",
3092
+ "Get detailed MQA (Metadata Quality Assurance) quality reasons for a dataset on dati.gov.it. Returns dimension scores, non-max reasons, and raw MQA flags from data.europa.eu. Only works with dati.gov.it server.",
3093
+ {
3094
+ server_url: z8.string().url().describe("Base URL of dati.gov.it (e.g., https://www.dati.gov.it/opendata)"),
3095
+ dataset_id: z8.string().describe("Dataset ID or name"),
3096
+ response_format: ResponseFormatSchema.optional()
3097
+ },
3098
+ async ({ server_url, dataset_id, response_format }) => {
3099
+ if (!isValidMqaServer(server_url)) {
3100
+ return {
3101
+ content: [{
3102
+ type: "text",
3103
+ text: `Error: MQA quality details are only available for dati.gov.it datasets. Provided server: ${server_url}
3104
+
3105
+ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and only evaluates datasets from Italian open data portal.`
3106
+ }]
3107
+ };
3108
+ }
3109
+ try {
3110
+ const details = await getMqaQualityDetails(server_url, dataset_id);
3111
+ const format = response_format || "markdown" /* MARKDOWN */;
3112
+ const output = format === "json" /* JSON */ ? JSON.stringify(details, null, 2) : formatQualityDetailsMarkdown(details, dataset_id);
3113
+ return {
3114
+ content: [{
3115
+ type: "text",
3116
+ text: output
3117
+ }]
3118
+ };
3119
+ } catch (error) {
3120
+ const errorMessage = error instanceof Error ? error.message : String(error);
3121
+ return {
3122
+ content: [{
3123
+ type: "text",
3124
+ text: `Error retrieving quality details: ${errorMessage}`
3125
+ }]
3126
+ };
3127
+ }
3128
+ }
3129
+ );
2865
3130
  }
2866
3131
 
2867
3132
  // src/resources/dataset.ts
@@ -3403,7 +3668,7 @@ var registerAllPrompts = (server2) => {
3403
3668
  function createServer() {
3404
3669
  return new McpServer({
3405
3670
  name: "ckan-mcp-server",
3406
- version: "0.4.33"
3671
+ version: "0.4.36"
3407
3672
  });
3408
3673
  }
3409
3674
  function registerAll(server2) {