@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 +10 -0
- package/dist/index.js +192 -10
- package/dist/worker.js +29 -29
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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}/
|
|
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(`
|
|
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.
|
|
3050
|
+
version: "0.4.22"
|
|
2869
3051
|
});
|
|
2870
3052
|
}
|
|
2871
3053
|
function registerAll(server2) {
|