@aborruso/ckan-mcp-server 0.4.33 → 0.4.35

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,27 @@
2
2
 
3
3
  ## 2026-02-01
4
4
 
5
+ ### Release v0.4.35
6
+
7
+ - Tests: adjust MQA metrics details fixture to include scoring entries
8
+ - Files: `tests/integration/quality.test.ts`
9
+
10
+ ### Unreleased
11
+
12
+ - None
13
+
14
+ ### Release v0.4.34
15
+
16
+ - MQA: add detailed quality reasons tool with metrics flag parsing
17
+ - MQA: add guidance note to use metrics endpoint for score deductions
18
+ - Tests: cover detailed MQA reasons output and guidance note
19
+ - Docs: list `ckan_get_mqa_quality_details` tool
20
+ - Files: `src/tools/quality.ts`, `tests/integration/quality.test.ts`, `README.md`, `docs/architecture-flow.md`
21
+
22
+ ### Unreleased
23
+
24
+ - None
25
+
5
26
  ### Release v0.4.33
6
27
 
7
28
  - 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
package/dist/worker.js CHANGED
@@ -627,7 +627,7 @@ Returns:
627
627
  `,i+=`| Group | Datasets |
628
628
  `,i+=`|-------|----------|
629
629
  `;for(let a of o)i+=`| ${a.display_name||a.name} | ${a.count} |
630
- `}return{content:[{type:"text",text:J(i)}]}}catch(r){return{content:[{type:"text",text:`Error searching groups: ${r instanceof Error?r.message:String(r)}`}],isError:!0}}})}var Av="https://data.europa.eu/api/mqa/cache/datasets",Nv="https://data.europa.eu/api/hub/repo/datasets",AO=[/^https?:\/\/(www\.)?dati\.gov\.it/i];function NO(t){return AO.some(e=>e.test(t))}function IO(t){return t.trim().replace(/:/g,"-").replace(/\./g,"-").replace(/-+/g,"-").toLowerCase()}function CO(t){let e=IO(t);if(!e)return[];let r=[e];return e.includes("~~")||r.push(`${e}~~1`,`${e}~~2`),r}var ln={accessibility:100,findability:100,interoperability:110,reusability:75,contextuality:20};function jO(t){if(!t||typeof t!="object")return;let e=t["@value"];if(typeof e=="number")return e;if(typeof e=="string"){let r=Number(e);return Number.isFinite(r)?r:void 0}}function MO(t){if(t&&typeof t=="object"&&"@graph"in t)return t;if(typeof t=="string")try{return JSON.parse(t)}catch{return}if(t instanceof ArrayBuffer)try{let e=new TextDecoder().decode(new Uint8Array(t));return JSON.parse(e)}catch{return}if(ArrayBuffer.isView(t))try{let e=t,r=new TextDecoder().decode(new Uint8Array(e.buffer,e.byteOffset,e.byteLength));return JSON.parse(r)}catch{return}return t}function ZO(t){let e={},r=MO(t);if(!r||typeof r!="object")return e;let n=r["@graph"];if(!Array.isArray(n))return e;for(let o of n){if(!o||typeof o!="object")continue;let s=o["dqv:isMeasurementOf"];if(!s)continue;let i=typeof s=="string"?s:s["@id"];if(typeof i!="string")continue;let a=jO(o["dqv:value"]);a!==void 0&&(i.endsWith("#accessibilityScoring")?e.accessibility=a:i.endsWith("#findabilityScoring")?e.findability=a:i.endsWith("#interoperabilityScoring")?e.interoperability=a:i.endsWith("#reusabilityScoring")?e.reusability=a:i.endsWith("#contextualityScoring")?e.contextuality=a:i.endsWith("#scoring")&&(e.total=a))}return e}function DO(t){let e=[];return Object.keys(ln).forEach(r=>{let n=t[r];typeof n=="number"&&n<ln[r]&&e.push(r)}),e}async function qO(t,e){let r=await ne(t,"package_show",{id:e}),n=r.identifier||r.name,o=CO(n);if(o.length===0)throw new Error("Dataset identifier is empty; cannot query MQA API");for(let s of o){let i=`${Av}/${s}`,a=`${Nv}/${s}/metrics`;try{let c=await Sr.get(i,{timeout:3e4,headers:{"User-Agent":"CKAN-MCP-Server/1.0"}}),u;try{let p=await fetch(a,{headers:{"User-Agent":"CKAN-MCP-Server/1.0"}});if(!p.ok)throw new Error(`MQA metrics error: ${p.status} ${p.statusText}`);try{u=await p.json()}catch{u=await p.text()}}catch(p){throw p instanceof Error?new Error(`MQA metrics error: ${p.message}`):p}let l=ZO(u),h=c.data?.result?.results?.[0]?.info?.["dataset-id"]||s,f={scores:l,nonMaxDimensions:DO(l),metricsUrl:a,mqaUrl:i,portalId:h};return{mqa:c.data,breakdown:f}}catch(c){if(Sr.isAxiosError(c)){if(c.response?.status===404)continue;throw new Error(`MQA API error: ${c.message}`)}throw c}}throw new Error(`Quality metrics not found or identifier not aligned on data.europa.eu. Tried: ${o.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').`)}function jp(t,e){if(Array.isArray(t)){for(let r of t)if(r&&typeof r=="object"&&e in r)return r[e]}}function Ov(t){if(!Array.isArray(t))return;let e=new Map;for(let r of t){if(!r||typeof r!="object")continue;let n=r.name,o=r.percentage;typeof n=="string"&&typeof o=="number"&&e.set(n.toLowerCase(),o)}if(e.has("yes"))return(e.get("yes")||0)>0;if(e.size>0){for(let[r,n]of e.entries())if(r.startsWith("2")&&n>0)return!0;return!1}}function dt(t,e){let r=jp(t,e);if(typeof r=="boolean")return r}function un(t,e,r){let n=jp(t,e),o=Ov(n);if(o!==void 0)return{available:o};if(r){let s=jp(t,r),i=Ov(s);if(i!==void 0)return{available:i}}}function LO(t){let e=t?.mqa??t,r=t?.breakdown,n=e?.result?.results?.[0];return!n||typeof n!="object"?{...t,breakdown:r}:{id:n.info?.["dataset-id"],info:{score:n.info?.score},accessibility:{accessUrl:un(n.accessibility,"accessUrlAvailability","accessUrlStatusCode"),downloadUrl:un(n.accessibility,"downloadUrlAvailability","downloadUrlStatusCode")},reusability:{licence:un(n.reusability,"licenceAvailability"),contactPoint:dt(n.reusability,"contactPointAvailability")!==void 0?{available:dt(n.reusability,"contactPointAvailability")}:void 0,publisher:dt(n.reusability,"publisherAvailability")!==void 0?{available:dt(n.reusability,"publisherAvailability")}:void 0},interoperability:{format:un(n.interoperability,"formatAvailability"),mediaType:un(n.interoperability,"mediaTypeAvailability")},findability:{keyword:dt(n.findability,"keywordAvailability")!==void 0?{available:dt(n.findability,"keywordAvailability")}:void 0,category:dt(n.findability,"categoryAvailability")!==void 0?{available:dt(n.findability,"categoryAvailability")}:void 0,spatial:dt(n.findability,"spatialAvailability")!==void 0?{available:dt(n.findability,"spatialAvailability")}:void 0,temporal:dt(n.findability,"temporalAvailability")!==void 0?{available:dt(n.findability,"temporalAvailability")}:void 0},contextuality:{byteSize:un(n.contextuality,"byteSizeAvailability"),rights:un(n.contextuality,"rightsAvailability")},breakdown:r}}function UO(t,e){let r=LO(t),n=[];if(n.push(`# Quality Metrics for Dataset: ${e}`),n.push(""),r.info?.score!==void 0&&(n.push(`**Overall Score**: ${r.info.score}/405`),n.push("")),r.breakdown?.scores){n.push("## Dimension Scores");let s=r.breakdown.scores,i=[["accessibility","Accessibility",ln.accessibility],["findability","Findability",ln.findability],["interoperability","Interoperability",ln.interoperability],["reusability","Reusability",ln.reusability],["contextuality","Contextuality",ln.contextuality]];for(let[a,c,u]of i){let l=s[a];if(typeof l=="number"){let d=l>=u,h=d?"\u2705":"\u26A0\uFE0F";n.push(`- ${c}: ${l}/${u} ${h}${d?"":` (max ${u})`}`)}}r.breakdown.nonMaxDimensions.length>0?n.push(`- Non-max dimension(s): ${r.breakdown.nonMaxDimensions.join(", ")}`):Object.keys(s).length>0&&n.push("- Non-max dimension(s): none"),n.push("")}r.accessibility&&(n.push("## Accessibility"),r.accessibility.accessUrl!==void 0&&n.push(`- Access URL: ${r.accessibility.accessUrl.available?"\u2713":"\u2717"} Available`),r.accessibility.downloadUrl!==void 0&&n.push(`- Download URL: ${r.accessibility.downloadUrl.available?"\u2713":"\u2717"} Available`),n.push("")),r.reusability&&(n.push("## Reusability"),r.reusability.licence!==void 0&&n.push(`- License: ${r.reusability.licence.available?"\u2713":"\u2717"} Available`),r.reusability.contactPoint!==void 0&&n.push(`- Contact Point: ${r.reusability.contactPoint.available?"\u2713":"\u2717"} Available`),r.reusability.publisher!==void 0&&n.push(`- Publisher: ${r.reusability.publisher.available?"\u2713":"\u2717"} Available`),n.push("")),r.interoperability&&(n.push("## Interoperability"),r.interoperability.format!==void 0&&n.push(`- Format: ${r.interoperability.format.available?"\u2713":"\u2717"} Available`),r.interoperability.mediaType!==void 0&&n.push(`- Media Type: ${r.interoperability.mediaType.available?"\u2713":"\u2717"} Available`),n.push("")),r.findability&&(n.push("## Findability"),r.findability.keyword!==void 0&&n.push(`- Keywords: ${r.findability.keyword.available?"\u2713":"\u2717"} Available`),r.findability.category!==void 0&&n.push(`- Category: ${r.findability.category.available?"\u2713":"\u2717"} Available`),r.findability.spatial!==void 0&&n.push(`- Spatial: ${r.findability.spatial.available?"\u2713":"\u2717"} Available`),r.findability.temporal!==void 0&&n.push(`- Temporal: ${r.findability.temporal.available?"\u2713":"\u2717"} Available`),n.push("")),r.contextuality&&(n.push("## Contextuality"),r.contextuality.byteSize!==void 0&&n.push(`- Byte Size: ${r.contextuality.byteSize.available?"\u2713":"\u2717"} Available`),r.contextuality.rights!==void 0&&n.push(`- Rights: ${r.contextuality.rights.available?"\u2713":"\u2717"} Available`),n.push("")),n.push("---");let o=r.breakdown?.portalId||r.id||e;return n.push(`Portal: https://data.europa.eu/data/datasets/${o}/quality?locale=it`),n.push(`MQA source: ${Av}/${o}`),n.push(`Metrics endpoint: ${r.breakdown?.metricsUrl||`${Nv}/${o}/metrics`}`),n.join(`
630
+ `}return{content:[{type:"text",text:J(i)}]}}catch(r){return{content:[{type:"text",text:`Error searching groups: ${r instanceof Error?r.message:String(r)}`}],isError:!0}}})}var Av="https://data.europa.eu/api/mqa/cache/datasets",Nv="https://data.europa.eu/api/hub/repo/datasets",AO=[/^https?:\/\/(www\.)?dati\.gov\.it/i];function NO(t){return AO.some(e=>e.test(t))}function IO(t){return t.trim().replace(/:/g,"-").replace(/\./g,"-").replace(/-+/g,"-").toLowerCase()}function CO(t){let e=IO(t);if(!e)return[];let r=[e];return e.includes("~~")||r.push(`${e}~~1`,`${e}~~2`),r}var ln={accessibility:100,findability:100,interoperability:110,reusability:75,contextuality:20};function jO(t){if(!t||typeof t!="object")return;let e=t["@value"];if(typeof e=="number")return e;if(typeof e=="string"){let r=Number(e);return Number.isFinite(r)?r:void 0}}function MO(t){if(t&&typeof t=="object"&&"@graph"in t)return t;if(typeof t=="string")try{return JSON.parse(t)}catch{return}if(t instanceof ArrayBuffer)try{let e=new TextDecoder().decode(new Uint8Array(t));return JSON.parse(e)}catch{return}if(ArrayBuffer.isView(t))try{let e=t,r=new TextDecoder().decode(new Uint8Array(e.buffer,e.byteOffset,e.byteLength));return JSON.parse(r)}catch{return}return t}function ZO(t){let e={},r=MO(t);if(!r||typeof r!="object")return e;let n=r["@graph"];if(!Array.isArray(n))return e;for(let o of n){if(!o||typeof o!="object")continue;let s=o["dqv:isMeasurementOf"];if(!s)continue;let i=typeof s=="string"?s:s["@id"];if(typeof i!="string")continue;let a=jO(o["dqv:value"]);a!==void 0&&(i.endsWith("#accessibilityScoring")?e.accessibility=a:i.endsWith("#findabilityScoring")?e.findability=a:i.endsWith("#interoperabilityScoring")?e.interoperability=a:i.endsWith("#reusabilityScoring")?e.reusability=a:i.endsWith("#contextualityScoring")?e.contextuality=a:i.endsWith("#scoring")&&(e.total=a))}return e}function DO(t){let e=[];return Object.keys(ln).forEach(r=>{let n=t[r];typeof n=="number"&&n<ln[r]&&e.push(r)}),e}async function qO(t,e){let r=await ne(t,"package_show",{id:e}),n=r.identifier||r.name,o=CO(n);if(o.length===0)throw new Error("Dataset identifier is empty; cannot query MQA API");for(let s of o){let i=`${Av}/${s}`,a=`${Nv}/${s}/metrics`;try{let c=await Sr.get(i,{timeout:3e4,headers:{"User-Agent":"CKAN-MCP-Server/1.0"}}),u;try{let p=await fetch(a,{headers:{"User-Agent":"CKAN-MCP-Server/1.0"}});if(!p.ok)throw new Error(`MQA metrics error: ${p.status} ${p.statusText}`);try{u=await p.json()}catch{u=await p.text()}}catch(p){throw p instanceof Error?new Error(`MQA metrics error: ${p.message}`):p}let l=ZO(u),h=c.data?.result?.results?.[0]?.info?.["dataset-id"]||s,f={scores:l,nonMaxDimensions:DO(l),metricsUrl:a,mqaUrl:i,portalId:h};return{mqa:c.data,breakdown:f}}catch(c){if(Sr.isAxiosError(c)){if(c.response?.status===404)continue;throw new Error(`MQA API error: ${c.message}`)}throw c}}throw new Error(`Quality metrics not found or identifier not aligned on data.europa.eu. Tried: ${o.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').`)}function jp(t,e){if(Array.isArray(t)){for(let r of t)if(r&&typeof r=="object"&&e in r)return r[e]}}function Ov(t){if(!Array.isArray(t))return;let e=new Map;for(let r of t){if(!r||typeof r!="object")continue;let n=r.name,o=r.percentage;typeof n=="string"&&typeof o=="number"&&e.set(n.toLowerCase(),o)}if(e.has("yes"))return(e.get("yes")||0)>0;if(e.size>0){for(let[r,n]of e.entries())if(r.startsWith("2")&&n>0)return!0;return!1}}function dt(t,e){let r=jp(t,e);if(typeof r=="boolean")return r}function un(t,e,r){let n=jp(t,e),o=Ov(n);if(o!==void 0)return{available:o};if(r){let s=jp(t,r),i=Ov(s);if(i!==void 0)return{available:i}}}function LO(t){let e=t?.mqa??t,r=t?.breakdown,n=e?.result?.results?.[0];return!n||typeof n!="object"?{...t,breakdown:r}:{id:n.info?.["dataset-id"],info:{score:n.info?.score},accessibility:{accessUrl:un(n.accessibility,"accessUrlAvailability","accessUrlStatusCode"),downloadUrl:un(n.accessibility,"downloadUrlAvailability","downloadUrlStatusCode")},reusability:{licence:un(n.reusability,"licenceAvailability"),contactPoint:dt(n.reusability,"contactPointAvailability")!==void 0?{available:dt(n.reusability,"contactPointAvailability")}:void 0,publisher:dt(n.reusability,"publisherAvailability")!==void 0?{available:dt(n.reusability,"publisherAvailability")}:void 0},interoperability:{format:un(n.interoperability,"formatAvailability"),mediaType:un(n.interoperability,"mediaTypeAvailability")},findability:{keyword:dt(n.findability,"keywordAvailability")!==void 0?{available:dt(n.findability,"keywordAvailability")}:void 0,category:dt(n.findability,"categoryAvailability")!==void 0?{available:dt(n.findability,"categoryAvailability")}:void 0,spatial:dt(n.findability,"spatialAvailability")!==void 0?{available:dt(n.findability,"spatialAvailability")}:void 0,temporal:dt(n.findability,"temporalAvailability")!==void 0?{available:dt(n.findability,"temporalAvailability")}:void 0},contextuality:{byteSize:un(n.contextuality,"byteSizeAvailability"),rights:un(n.contextuality,"rightsAvailability")},breakdown:r}}function UO(t,e){let r=LO(t),n=[];if(n.push(`# Quality Metrics for Dataset: ${e}`),n.push(""),r.info?.score!==void 0&&(n.push(`**Overall Score**: ${r.info.score}/405`),n.push("")),r.breakdown?.scores){n.push("## Dimension Scores");let i=r.breakdown.scores,a=[["accessibility","Accessibility",ln.accessibility],["findability","Findability",ln.findability],["interoperability","Interoperability",ln.interoperability],["reusability","Reusability",ln.reusability],["contextuality","Contextuality",ln.contextuality]];for(let[c,u,l]of a){let d=i[c];if(typeof d=="number"){let h=d>=l,f=h?"\u2705":"\u26A0\uFE0F";n.push(`- ${u}: ${d}/${l} ${f}${h?"":` (max ${l})`}`)}}r.breakdown.nonMaxDimensions.length>0?n.push(`- Non-max dimension(s): ${r.breakdown.nonMaxDimensions.join(", ")}`):Object.keys(i).length>0&&n.push("- Non-max dimension(s): none"),n.push("")}r.accessibility&&(n.push("## Accessibility"),r.accessibility.accessUrl!==void 0&&n.push(`- Access URL: ${r.accessibility.accessUrl.available?"\u2713":"\u2717"} Available`),r.accessibility.downloadUrl!==void 0&&n.push(`- Download URL: ${r.accessibility.downloadUrl.available?"\u2713":"\u2717"} Available`),n.push("")),r.reusability&&(n.push("## Reusability"),r.reusability.licence!==void 0&&n.push(`- License: ${r.reusability.licence.available?"\u2713":"\u2717"} Available`),r.reusability.contactPoint!==void 0&&n.push(`- Contact Point: ${r.reusability.contactPoint.available?"\u2713":"\u2717"} Available`),r.reusability.publisher!==void 0&&n.push(`- Publisher: ${r.reusability.publisher.available?"\u2713":"\u2717"} Available`),n.push("")),r.interoperability&&(n.push("## Interoperability"),r.interoperability.format!==void 0&&n.push(`- Format: ${r.interoperability.format.available?"\u2713":"\u2717"} Available`),r.interoperability.mediaType!==void 0&&n.push(`- Media Type: ${r.interoperability.mediaType.available?"\u2713":"\u2717"} Available`),n.push("")),r.findability&&(n.push("## Findability"),r.findability.keyword!==void 0&&n.push(`- Keywords: ${r.findability.keyword.available?"\u2713":"\u2717"} Available`),r.findability.category!==void 0&&n.push(`- Category: ${r.findability.category.available?"\u2713":"\u2717"} Available`),r.findability.spatial!==void 0&&n.push(`- Spatial: ${r.findability.spatial.available?"\u2713":"\u2717"} Available`),r.findability.temporal!==void 0&&n.push(`- Temporal: ${r.findability.temporal.available?"\u2713":"\u2717"} Available`),n.push("")),r.contextuality&&(n.push("## Contextuality"),r.contextuality.byteSize!==void 0&&n.push(`- Byte Size: ${r.contextuality.byteSize.available?"\u2713":"\u2717"} Available`),r.contextuality.rights!==void 0&&n.push(`- Rights: ${r.contextuality.rights.available?"\u2713":"\u2717"} Available`),n.push("")),n.push("---");let o=r.breakdown?.portalId||r.id||e;n.push(`Portal: https://data.europa.eu/data/datasets/${o}/quality?locale=it`),n.push(`MQA source: ${Av}/${o}`);let s=r.breakdown?.metricsUrl||`${Nv}/${o}/metrics`;return n.push(`Metrics endpoint: ${s}`),n.push("Tip: Use the metrics endpoint to explain score deductions (e.g., failing measurements such as knownLicence = false)."),n.join(`
631
631
  `)}function Iv(t){t.tool("ckan_get_mqa_quality","Get MQA (Metadata Quality Assurance) quality metrics for a dataset on dati.gov.it. Returns quality score and detailed metrics (accessibility, reusability, interoperability, findability, contextuality) from data.europa.eu. Only works with dati.gov.it server.",{server_url:$.string().url().describe("Base URL of dati.gov.it (e.g., https://www.dati.gov.it/opendata)"),dataset_id:$.string().describe("Dataset ID or name"),response_format:ke.optional()},async({server_url:e,dataset_id:r,response_format:n})=>{if(!NO(e))return{content:[{type:"text",text:`Error: MQA quality metrics are only available for dati.gov.it datasets. Provided server: ${e}
632
632
 
633
633
  The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and only evaluates datasets from Italian open data portal.`}]};try{let o=await qO(e,r);return{content:[{type:"text",text:(n||"markdown")==="json"?JSON.stringify(o,null,2):UO(o,r)}]}}catch(o){return{content:[{type:"text",text:`Error retrieving quality metrics: ${o instanceof Error?o.message:String(o)}`}]}}})}function kr(t){let e=t.hostname;if(!e)throw new Error("Invalid ckan:// URI: missing server hostname");let r=t.pathname.split("/").filter(a=>a.length>0);if(r.length<2)throw new Error(`Invalid ckan:// URI: expected /{type}/{id}, got ${t.pathname}`);let[n,...o]=r,s=o.join("/");if(!n||!s)throw new Error("Invalid ckan:// URI: missing type or id");return{server:Sa(e)||`https://${e}`,type:n,id:s}}function Cv(t){t.registerResource("ckan-dataset",new Ct("ckan://{server}/dataset/{id}",{list:void 0}),{title:"CKAN Dataset",description:"Access dataset metadata from any CKAN server. URI format: ckan://{server}/dataset/{id}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=kr(e),o=r.id,s=await ne(n,"package_show",{id:o}),i=J(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching dataset: ${o}`}]}}})}function jv(t){t.registerResource("ckan-resource",new Ct("ckan://{server}/resource/{id}",{list:void 0}),{title:"CKAN Resource",description:"Access resource metadata and download URL from any CKAN server. URI format: ckan://{server}/resource/{id}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=kr(e),o=r.id,s=await ne(n,"resource_show",{id:o}),i=J(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching resource: ${o}`}]}}})}function Mv(t){t.registerResource("ckan-organization",new Ct("ckan://{server}/organization/{name}",{list:void 0}),{title:"CKAN Organization",description:"Access organization metadata from any CKAN server. URI format: ckan://{server}/organization/{name}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=kr(e),o=r.name,s=await ne(n,"organization_show",{id:o,include_datasets:!1}),i=J(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching organization: ${o}`}]}}})}var za=t=>t.replace(/["\\]/g,"\\$&"),FO=t=>{let e=t.trim(),r=[e],n=e.toUpperCase();return n!==e&&r.push(n),`(${["res_format","distribution_format"].flatMap(i=>r.map(a=>`${i}:"${za(a)}"`)).join(" OR ")})`},Ta=(t,e)=>{t.registerResource(e.name,new Ct(e.template,{list:void 0}),{title:e.title,description:e.description,mimeType:"application/json"},async(r,n)=>{try{let{server:o}=kr(r),s=e.buildFq(n),i=await ne(o,"package_search",{q:"*:*",fq:s}),a=J(JSON.stringify(i,null,2));return{contents:[{uri:r.href,mimeType:"application/json",text:a}]}}catch(o){let s=o instanceof Error?o.message:String(o);return{contents:[{uri:r.href,mimeType:"text/plain",text:`Error fetching datasets: ${s}`}]}}})};function Zv(t){Ta(t,{name:"ckan-group-datasets",template:"ckan://{server}/group/{name}/datasets",title:"CKAN Group Datasets",description:"List datasets in a CKAN group (theme). URI format: ckan://{server}/group/{name}/datasets",buildFq:e=>`groups:"${za(e.name)}"`})}function Dv(t){Ta(t,{name:"ckan-organization-datasets",template:"ckan://{server}/organization/{name}/datasets",title:"CKAN Organization Datasets",description:"List datasets for a CKAN organization. URI format: ckan://{server}/organization/{name}/datasets",buildFq:e=>`organization:"${za(e.name)}"`})}function qv(t){Ta(t,{name:"ckan-tag-datasets",template:"ckan://{server}/tag/{name}/datasets",title:"CKAN Tag Datasets",description:"List datasets matching a CKAN tag. URI format: ckan://{server}/tag/{name}/datasets",buildFq:e=>`tags:"${za(e.name)}"`})}function Lv(t){Ta(t,{name:"ckan-format-datasets",template:"ckan://{server}/format/{format}/datasets",title:"CKAN Format Datasets",description:"List datasets by resource format. URI format: ckan://{server}/format/{format}/datasets",buildFq:e=>FO(e.format)})}function Uv(t){Cv(t),jv(t),Mv(t),Zv(t),Dv(t),qv(t),Lv(t)}var Zt=t=>({messages:[{role:"user",content:{type:"text",text:t}}]});var VO="ckan-search-by-theme",HO=(t,e,r)=>`# Guided search: datasets by theme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aborruso/ckan-mcp-server",
3
- "version": "0.4.33",
3
+ "version": "0.4.35",
4
4
  "description": "MCP server for interacting with CKAN open data portals",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",