@aborruso/ckan-mcp-server 0.4.125 → 0.4.126

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.
Files changed (3) hide show
  1. package/LOG.md +10 -0
  2. package/dist/index.js +65 -6
  3. package/package.json +1 -1
package/LOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## 2026-09-23
4
4
 
5
+ ### v0.4.126 - why MQA URL tests fail
6
+
7
+ Ships #552. For a caller: `ckan_get_mqa_quality_details` now says why URL tests fail (`HTTP test: 1100 timeout ×21, no status recorded ×3`). For the project: `npm run smoke` checks the MQA tool live against data.europa.eu, so a format change there fails the release gate.
8
+
9
+ ### MQA: smoke gate and HTTP status of failing URL tests
10
+
11
+ - `npm run smoke` gains a live MQA case (Roma `czrm-…-2023`: `methodology` v2, `maxScore` 7.5, 0 ≤ `score` ≤ 7.5) and a generic `field_max`. v0.4.124 shipped broken MQA tools with green tests because the tests read stored fixtures; the gate now asks data.europa.eu. Negative check: expecting `v1` fails the case.
12
+ - `ckan_get_mqa_quality_details` says why URL tests fail: `HTTP test: 1100 timeout ×21, no status recorded ×3`. Codes come from the metrics graph (`http:statusCodeValue` in v2, `dqv:value` in v1; ≥1000 are piveau's own, 1100 = timeout), latest per distribution, 2xx excluded. Fetched only by the details tool and only when a URL test fails (the graph is ~570 KB); best effort, a failed fetch leaves the details intact.
13
+ - Bug caught by the tests: boolean result nodes share the metric id, and `Number(false)` read as status code 0.
14
+
5
15
  ### v0.4.125 - MQA scores on the new data.europa.eu methodology
6
16
 
7
17
  Ships #549 (closes #548). For a caller: `ckan_get_mqa_quality` and `ckan_get_mqa_quality_details` report the MQA v2 score (0-7.5, band, failing metrics by gain) instead of printing v2 numbers on the old 405 scale or failing with "identifier not aligned"; datasets not yet re-evaluated get the previous-methodology score, labelled. The MQA tools work again on the Cloudflare Worker. **BREAKING** for JSON consumers of the two MQA tools: compact shape with `methodology`, `metricsVersion`, `score`, `maxScore`.
package/dist/index.js CHANGED
@@ -4497,7 +4497,55 @@ async function fetchMqaV1(europeanId, links) {
4497
4497
  ...links
4498
4498
  };
4499
4499
  }
4500
- async function getMqaQuality(serverUrl, datasetId) {
4500
+ var STATUS_METRICS = /* @__PURE__ */ new Set(["accessUrlStatusCode", "downloadUrlStatusCode"]);
4501
+ var HTTP_STATUS_CODE = "http://www.w3.org/2011/http#statusCodeValue";
4502
+ function extractStatusCodes(metricsData) {
4503
+ const latest = /* @__PURE__ */ new Map();
4504
+ const graph = decodeMetricsPayload(metricsData)?.["@graph"];
4505
+ for (const node of Array.isArray(graph) ? graph : []) {
4506
+ if (!node || typeof node !== "object") continue;
4507
+ const record = node;
4508
+ const metricRef = record["dqv:isMeasurementOf"];
4509
+ const metricId = typeof metricRef === "string" ? metricRef : metricRef?.["@id"];
4510
+ if (typeof metricId !== "string") continue;
4511
+ const metric = metricKeyFromId(metricId);
4512
+ if (!STATUS_METRICS.has(metric)) continue;
4513
+ const raw = record[HTTP_STATUS_CODE] !== void 0 ? Number(record[HTTP_STATUS_CODE]) : parseMetricValue(record["dqv:value"]);
4514
+ if (typeof raw !== "number" || !Number.isInteger(raw)) continue;
4515
+ const code = raw;
4516
+ const target = record["dqv:computedOn"]?.["@id"];
4517
+ const time = String(record["prov:generatedAtTime"]?.["@value"] ?? "");
4518
+ const key = `${metric} ${typeof target === "string" ? target : ""}`;
4519
+ const current = latest.get(key);
4520
+ if (current && current.time > time) continue;
4521
+ const description = typeof record["dct:description"] === "string" ? record["dct:description"] : "";
4522
+ const label = code >= 1e3 ? `${code} ${/timeout/i.test(description) ? "timeout" : "connection error"}` : String(code);
4523
+ latest.set(key, { metric, code, label, time });
4524
+ }
4525
+ const counts = /* @__PURE__ */ new Map();
4526
+ for (const { metric, code, label } of latest.values()) {
4527
+ if (code >= 200 && code < 300) continue;
4528
+ const perMetric = counts.get(metric) ?? {};
4529
+ perMetric[label] = (perMetric[label] ?? 0) + 1;
4530
+ counts.set(metric, perMetric);
4531
+ }
4532
+ return counts;
4533
+ }
4534
+ async function addHttpStatus(result) {
4535
+ const urlTests = result.failing.filter((item) => item.entity === "distribution" && STATUS_METRICS.has(item.metric));
4536
+ if (urlTests.length === 0) return;
4537
+ let counts;
4538
+ try {
4539
+ counts = extractStatusCodes(await fetchMetricsGraph(`${MQA_METRICS_BASE}/${result.portalId}/metrics`));
4540
+ } catch {
4541
+ return;
4542
+ }
4543
+ for (const item of urlTests) {
4544
+ const perCode = counts.get(item.metric);
4545
+ if (perCode) item.httpStatus = perCode;
4546
+ }
4547
+ }
4548
+ async function getMqaQuality(serverUrl, datasetId, opts = {}) {
4501
4549
  const dataset = await makeCkanRequest(
4502
4550
  serverUrl,
4503
4551
  "package_show",
@@ -4534,7 +4582,11 @@ async function getMqaQuality(serverUrl, datasetId) {
4534
4582
  if (!entry || typeof entry !== "object") {
4535
4583
  throw new Error("Unexpected MQA payload: no result");
4536
4584
  }
4537
- return parseMqaV2(entry, links);
4585
+ const result = parseMqaV2(entry, links);
4586
+ if (opts.httpStatus) {
4587
+ await addHttpStatus(result);
4588
+ }
4589
+ return result;
4538
4590
  }
4539
4591
  throw new Error(
4540
4592
  `No MQA record on data.europa.eu for this dataset (identifier may not be aligned). 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').`
@@ -4545,7 +4597,14 @@ function formatNumber(value) {
4545
4597
  }
4546
4598
  function describeFailing(item) {
4547
4599
  const scope = item.entity === "dataset" ? "dataset" : `${item.failed} of ${item.total} ${item.entity === "distribution" ? "distributions" : "data services"}`;
4548
- return `\`${item.property}\` (${item.metric}, ${item.importance.toLowerCase()} ${item.weight}) - ${scope}, +${formatNumber(item.gain)}`;
4600
+ let http = "";
4601
+ if (item.httpStatus) {
4602
+ const codes = Object.entries(item.httpStatus).sort((a, b) => b[1] - a[1]).map(([code, n]) => `${code} \xD7${n}`);
4603
+ const unexplained = item.failed - Object.values(item.httpStatus).reduce((sum, n) => sum + n, 0);
4604
+ if (unexplained > 0) codes.push(`no status recorded \xD7${unexplained}`);
4605
+ http = `; HTTP test: ${codes.join(", ")}`;
4606
+ }
4607
+ return `\`${item.property}\` (${item.metric}, ${item.importance.toLowerCase()} ${item.weight}) - ${scope}, +${formatNumber(item.gain)}${http}`;
4549
4608
  }
4550
4609
  function pushV2Scores(lines, result) {
4551
4610
  lines.push(`**Score**: ${formatNumber(result.score)}/${result.maxScore} (${result.band})`);
@@ -4704,7 +4763,7 @@ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and on
4704
4763
  "ckan_get_mqa_quality_details",
4705
4764
  {
4706
4765
  title: "Get MQA Quality Details",
4707
- description: "Get detailed MQA (Metadata Quality Assessment) quality reasons for a dataset on dati.gov.it. Lists every failing metric grouped by FAIR dimension, with DCAT-AP property, weight, how many distributions fail it and its gain on the final score (methodology v2); previous-methodology datasets get non-max reasons. Only works with dati.gov.it server. Typical workflow: ckan_get_mqa_quality (get overview scores) \u2192 ckan_get_mqa_quality_details (inspect failing metrics)",
4766
+ description: "Get detailed MQA (Metadata Quality Assessment) quality reasons for a dataset on dati.gov.it. Lists every failing metric grouped by FAIR dimension, with DCAT-AP property, weight, how many distributions fail it and its gain on the final score, plus the HTTP status of failing URL tests (e.g. 1100 timeout, 404) (methodology v2); previous-methodology datasets get non-max reasons. Only works with dati.gov.it server. Typical workflow: ckan_get_mqa_quality (get overview scores) \u2192 ckan_get_mqa_quality_details (inspect failing metrics)",
4708
4767
  inputSchema: z8.object({
4709
4768
  server_url: z8.string().url().describe("Base URL of dati.gov.it (e.g., https://www.dati.gov.it/opendata)"),
4710
4769
  dataset_id: z8.string().describe("Dataset ID or name"),
@@ -4733,7 +4792,7 @@ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and on
4733
4792
  };
4734
4793
  }
4735
4794
  try {
4736
- const details = await getMqaQuality(server_url, dataset_id);
4795
+ const details = await getMqaQuality(server_url, dataset_id, { httpStatus: true });
4737
4796
  const format = response_format || "markdown" /* MARKDOWN */;
4738
4797
  const output = format === "json" /* JSON */ ? truncateJson(details) : truncateText(addDemoFooter(formatQualityDetailsMarkdown(details, dataset_id)));
4739
4798
  return {
@@ -5935,7 +5994,7 @@ var registerAllPrompts = (server) => {
5935
5994
  function createServer() {
5936
5995
  return new McpServer({
5937
5996
  name: "ckan-mcp-server",
5938
- version: "0.4.125"
5997
+ version: "0.4.126"
5939
5998
  });
5940
5999
  }
5941
6000
  function registerAll(server) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aborruso/ckan-mcp-server",
3
- "version": "0.4.125",
3
+ "version": "0.4.126",
4
4
  "mcpName": "io.github.aborruso/ckan-mcp-server",
5
5
  "description": "MCP server for interacting with CKAN open data portals",
6
6
  "repository": {