@aborruso/ckan-mcp-server 0.4.21 → 0.4.23

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,16 @@
2
2
 
3
3
  ## 2026-01-26
4
4
 
5
+ ### Release v0.4.23
6
+
7
+ - Fix MQA quality score maximum from 450 to 405 (correct max: 100+100+110+75+20)
8
+ - Files modified: `src/tools/quality.ts:442`, `tests/integration/quality.test.ts:279,302`
9
+
10
+ ### Release v0.4.22
11
+
12
+ - Fix MQA identifier normalization to handle dot separators (e.g. `c_g273:D.1727` -> `c_g273-d-1727`)
13
+ - Workers deploy: https://ckan-mcp-server.andy-pr.workers.dev (2026-01-26)
14
+
5
15
  ### Release v0.4.21
6
16
 
7
17
  - Fix metrics parsing in Workers by switching to fetch and mocking fetch in tests
package/dist/index.js CHANGED
@@ -2105,6 +2105,7 @@ Returns:
2105
2105
  import { z as z8 } from "zod";
2106
2106
  import axios2 from "axios";
2107
2107
  var MQA_API_BASE = "https://data.europa.eu/api/mqa/cache/datasets";
2108
+ var MQA_METRICS_BASE = "https://data.europa.eu/api/hub/repo/datasets";
2108
2109
  var ALLOWED_SERVER_PATTERNS = [
2109
2110
  /^https?:\/\/(www\.)?dati\.gov\.it/i
2110
2111
  ];
@@ -2112,7 +2113,7 @@ function isValidMqaServer(serverUrl) {
2112
2113
  return ALLOWED_SERVER_PATTERNS.some((pattern) => pattern.test(serverUrl));
2113
2114
  }
2114
2115
  function normalizeMqaIdentifier(identifier) {
2115
- return identifier.trim().replace(/:/g, "-").replace(/-+/g, "-").toLowerCase();
2116
+ return identifier.trim().replace(/:/g, "-").replace(/\./g, "-").replace(/-+/g, "-").toLowerCase();
2116
2117
  }
2117
2118
  function buildMqaIdCandidates(identifier) {
2118
2119
  const base = normalizeMqaIdentifier(identifier);
@@ -2125,6 +2126,109 @@ function buildMqaIdCandidates(identifier) {
2125
2126
  }
2126
2127
  return candidates;
2127
2128
  }
2129
+ var DIMENSION_MAX = {
2130
+ accessibility: 100,
2131
+ findability: 100,
2132
+ interoperability: 110,
2133
+ reusability: 75,
2134
+ contextuality: 20
2135
+ };
2136
+ function parseScoreValue(value) {
2137
+ if (!value || typeof value !== "object") {
2138
+ return void 0;
2139
+ }
2140
+ const raw = value["@value"];
2141
+ if (typeof raw === "number") {
2142
+ return raw;
2143
+ }
2144
+ if (typeof raw === "string") {
2145
+ const parsed = Number(raw);
2146
+ return Number.isFinite(parsed) ? parsed : void 0;
2147
+ }
2148
+ return void 0;
2149
+ }
2150
+ function decodeMetricsPayload(payload) {
2151
+ if (payload && typeof payload === "object" && "@graph" in payload) {
2152
+ return payload;
2153
+ }
2154
+ if (typeof payload === "string") {
2155
+ try {
2156
+ return JSON.parse(payload);
2157
+ } catch {
2158
+ return void 0;
2159
+ }
2160
+ }
2161
+ if (payload instanceof ArrayBuffer) {
2162
+ try {
2163
+ const text = new TextDecoder().decode(new Uint8Array(payload));
2164
+ return JSON.parse(text);
2165
+ } catch {
2166
+ return void 0;
2167
+ }
2168
+ }
2169
+ if (ArrayBuffer.isView(payload)) {
2170
+ try {
2171
+ const view = payload;
2172
+ const text = new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
2173
+ return JSON.parse(text);
2174
+ } catch {
2175
+ return void 0;
2176
+ }
2177
+ }
2178
+ return payload;
2179
+ }
2180
+ function extractMetricsScores(metricsData) {
2181
+ const scores = {};
2182
+ const parsed = decodeMetricsPayload(metricsData);
2183
+ if (!parsed || typeof parsed !== "object") {
2184
+ return scores;
2185
+ }
2186
+ const graph = parsed["@graph"];
2187
+ if (!Array.isArray(graph)) {
2188
+ return scores;
2189
+ }
2190
+ for (const node of graph) {
2191
+ if (!node || typeof node !== "object") {
2192
+ continue;
2193
+ }
2194
+ const metricRef = node["dqv:isMeasurementOf"];
2195
+ if (!metricRef) {
2196
+ continue;
2197
+ }
2198
+ const metricId = typeof metricRef === "string" ? metricRef : metricRef["@id"];
2199
+ if (typeof metricId !== "string") {
2200
+ continue;
2201
+ }
2202
+ const value = parseScoreValue(node["dqv:value"]);
2203
+ if (value === void 0) {
2204
+ continue;
2205
+ }
2206
+ if (metricId.endsWith("#accessibilityScoring")) {
2207
+ scores.accessibility = value;
2208
+ } else if (metricId.endsWith("#findabilityScoring")) {
2209
+ scores.findability = value;
2210
+ } else if (metricId.endsWith("#interoperabilityScoring")) {
2211
+ scores.interoperability = value;
2212
+ } else if (metricId.endsWith("#reusabilityScoring")) {
2213
+ scores.reusability = value;
2214
+ } else if (metricId.endsWith("#contextualityScoring")) {
2215
+ scores.contextuality = value;
2216
+ } else if (metricId.endsWith("#scoring")) {
2217
+ scores.total = value;
2218
+ }
2219
+ }
2220
+ return scores;
2221
+ }
2222
+ function findNonMaxDimensions(scores) {
2223
+ const nonMax = [];
2224
+ Object.keys(DIMENSION_MAX).forEach((dimension) => {
2225
+ const value = scores[dimension];
2226
+ if (typeof value === "number" && value < DIMENSION_MAX[dimension]) {
2227
+ nonMax.push(dimension);
2228
+ }
2229
+ });
2230
+ return nonMax;
2231
+ }
2128
2232
  async function getMqaQuality(serverUrl, datasetId) {
2129
2233
  const dataset = await makeCkanRequest(
2130
2234
  serverUrl,
@@ -2138,6 +2242,7 @@ async function getMqaQuality(serverUrl, datasetId) {
2138
2242
  }
2139
2243
  for (const europeanId of candidates) {
2140
2244
  const mqaUrl = `${MQA_API_BASE}/${europeanId}`;
2245
+ const metricsUrl = `${MQA_METRICS_BASE}/${europeanId}/metrics`;
2141
2246
  try {
2142
2247
  const response = await axios2.get(mqaUrl, {
2143
2248
  timeout: 3e4,
@@ -2145,7 +2250,41 @@ async function getMqaQuality(serverUrl, datasetId) {
2145
2250
  "User-Agent": "CKAN-MCP-Server/1.0"
2146
2251
  }
2147
2252
  });
2148
- return response.data;
2253
+ let metricsPayload;
2254
+ try {
2255
+ const metricsResponse = await fetch(metricsUrl, {
2256
+ headers: {
2257
+ "User-Agent": "CKAN-MCP-Server/1.0"
2258
+ }
2259
+ });
2260
+ if (!metricsResponse.ok) {
2261
+ throw new Error(`MQA metrics error: ${metricsResponse.status} ${metricsResponse.statusText}`);
2262
+ }
2263
+ try {
2264
+ metricsPayload = await metricsResponse.json();
2265
+ } catch {
2266
+ metricsPayload = await metricsResponse.text();
2267
+ }
2268
+ } catch (metricsError) {
2269
+ if (metricsError instanceof Error) {
2270
+ throw new Error(`MQA metrics error: ${metricsError.message}`);
2271
+ }
2272
+ throw metricsError;
2273
+ }
2274
+ const scores = extractMetricsScores(metricsPayload);
2275
+ const resultEntry = response.data?.result?.results?.[0];
2276
+ const portalId = resultEntry?.info?.["dataset-id"] || europeanId;
2277
+ const breakdown = {
2278
+ scores,
2279
+ nonMaxDimensions: findNonMaxDimensions(scores),
2280
+ metricsUrl,
2281
+ mqaUrl,
2282
+ portalId
2283
+ };
2284
+ return {
2285
+ mqa: response.data,
2286
+ breakdown
2287
+ };
2149
2288
  } catch (error) {
2150
2289
  if (axios2.isAxiosError(error)) {
2151
2290
  if (error.response?.status === 404) {
@@ -2222,9 +2361,11 @@ function metricAvailability(section, availabilityKey, statusKey) {
2222
2361
  return void 0;
2223
2362
  }
2224
2363
  function normalizeQualityData(data) {
2225
- const resultEntry = data?.result?.results?.[0];
2364
+ const mqaData = data?.mqa ?? data;
2365
+ const breakdown = data?.breakdown;
2366
+ const resultEntry = mqaData?.result?.results?.[0];
2226
2367
  if (!resultEntry || typeof resultEntry !== "object") {
2227
- return data;
2368
+ return { ...data, breakdown };
2228
2369
  }
2229
2370
  return {
2230
2371
  id: resultEntry.info?.["dataset-id"],
@@ -2247,7 +2388,12 @@ function normalizeQualityData(data) {
2247
2388
  category: metricBoolean(resultEntry.findability, "categoryAvailability") !== void 0 ? { available: metricBoolean(resultEntry.findability, "categoryAvailability") } : void 0,
2248
2389
  spatial: metricBoolean(resultEntry.findability, "spatialAvailability") !== void 0 ? { available: metricBoolean(resultEntry.findability, "spatialAvailability") } : void 0,
2249
2390
  temporal: metricBoolean(resultEntry.findability, "temporalAvailability") !== void 0 ? { available: metricBoolean(resultEntry.findability, "temporalAvailability") } : void 0
2250
- }
2391
+ },
2392
+ contextuality: {
2393
+ byteSize: metricAvailability(resultEntry.contextuality, "byteSizeAvailability"),
2394
+ rights: metricAvailability(resultEntry.contextuality, "rightsAvailability")
2395
+ },
2396
+ breakdown
2251
2397
  };
2252
2398
  }
2253
2399
  function formatQualityMarkdown(data, datasetId) {
@@ -2256,7 +2402,32 @@ function formatQualityMarkdown(data, datasetId) {
2256
2402
  lines.push(`# Quality Metrics for Dataset: ${datasetId}`);
2257
2403
  lines.push("");
2258
2404
  if (normalized.info?.score !== void 0) {
2259
- lines.push(`**Overall Score**: ${normalized.info.score}/405`);
2405
+ lines.push(`**Overall Score**: ${normalized.info.score}/450`);
2406
+ lines.push("");
2407
+ }
2408
+ if (normalized.breakdown?.scores) {
2409
+ lines.push("## Dimension Scores");
2410
+ const scores = normalized.breakdown.scores;
2411
+ const order = [
2412
+ ["accessibility", "Accessibility", DIMENSION_MAX.accessibility],
2413
+ ["findability", "Findability", DIMENSION_MAX.findability],
2414
+ ["interoperability", "Interoperability", DIMENSION_MAX.interoperability],
2415
+ ["reusability", "Reusability", DIMENSION_MAX.reusability],
2416
+ ["contextuality", "Contextuality", DIMENSION_MAX.contextuality]
2417
+ ];
2418
+ for (const [key, label, max] of order) {
2419
+ const value = scores[key];
2420
+ if (typeof value === "number") {
2421
+ const isMax = value >= max;
2422
+ const status = isMax ? "\u2705" : "\u26A0\uFE0F";
2423
+ lines.push(`- ${label}: ${value}/${max} ${status}${isMax ? "" : ` (max ${max})`}`);
2424
+ }
2425
+ }
2426
+ if (normalized.breakdown.nonMaxDimensions.length > 0) {
2427
+ lines.push(`- Non-max dimension(s): ${normalized.breakdown.nonMaxDimensions.join(", ")}`);
2428
+ } else if (Object.keys(scores).length > 0) {
2429
+ lines.push("- Non-max dimension(s): none");
2430
+ }
2260
2431
  lines.push("");
2261
2432
  }
2262
2433
  if (normalized.accessibility) {
@@ -2308,16 +2479,27 @@ function formatQualityMarkdown(data, datasetId) {
2308
2479
  }
2309
2480
  lines.push("");
2310
2481
  }
2482
+ if (normalized.contextuality) {
2483
+ lines.push("## Contextuality");
2484
+ if (normalized.contextuality.byteSize !== void 0) {
2485
+ lines.push(`- Byte Size: ${normalized.contextuality.byteSize.available ? "\u2713" : "\u2717"} Available`);
2486
+ }
2487
+ if (normalized.contextuality.rights !== void 0) {
2488
+ lines.push(`- Rights: ${normalized.contextuality.rights.available ? "\u2713" : "\u2717"} Available`);
2489
+ }
2490
+ lines.push("");
2491
+ }
2311
2492
  lines.push("---");
2312
- const portalId = normalized.id || datasetId;
2493
+ const portalId = normalized.breakdown?.portalId || normalized.id || datasetId;
2313
2494
  lines.push(`Portal: https://data.europa.eu/data/datasets/${portalId}/quality?locale=it`);
2314
- lines.push(`Source: ${MQA_API_BASE}/${portalId}`);
2495
+ lines.push(`MQA source: ${MQA_API_BASE}/${portalId}`);
2496
+ lines.push(`Metrics endpoint: ${normalized.breakdown?.metricsUrl || `${MQA_METRICS_BASE}/${portalId}/metrics`}`);
2315
2497
  return lines.join("\n");
2316
2498
  }
2317
2499
  function registerQualityTools(server2) {
2318
2500
  server2.tool(
2319
2501
  "ckan_get_mqa_quality",
2320
- "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.",
2502
+ "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.",
2321
2503
  {
2322
2504
  server_url: z8.string().url().describe("Base URL of dati.gov.it (e.g., https://www.dati.gov.it/opendata)"),
2323
2505
  dataset_id: z8.string().describe("Dataset ID or name"),
@@ -2865,7 +3047,7 @@ var registerAllPrompts = (server2) => {
2865
3047
  function createServer() {
2866
3048
  return new McpServer({
2867
3049
  name: "ckan-mcp-server",
2868
- version: "0.4.17"
3050
+ version: "0.4.22"
2869
3051
  });
2870
3052
  }
2871
3053
  function registerAll(server2) {