@aborruso/ckan-mcp-server 0.4.32 → 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/AGENTS.md +7 -0
- package/LOG.md +26 -0
- package/README.md +1 -0
- package/dist/index.js +328 -6
- package/dist/worker.js +35 -29
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -45,6 +45,13 @@ Keep this managed block so 'openspec update' can refresh the instructions.
|
|
|
45
45
|
|
|
46
46
|
**Single test**: `npm test -- tests/unit/http.test.ts` | `npm test -- -t "testName"`
|
|
47
47
|
|
|
48
|
+
## Local MCP Client Build Test
|
|
49
|
+
|
|
50
|
+
Before deploying, you can test the current dev build by pointing your MCP client at the Node entrypoint in `dist/`:
|
|
51
|
+
|
|
52
|
+
1. Build: `npm run build`
|
|
53
|
+
2. Example absolute path: `/home/aborruso/git/idee/ckan-mcp-server/dist/index.js` (adjust to your local checkout)
|
|
54
|
+
|
|
48
55
|
## GitHub CLI Notes
|
|
49
56
|
|
|
50
57
|
When creating issues with multi-line bodies, avoid literal `\n` in `--body`. Use a here-doc
|
package/LOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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
|
+
|
|
26
|
+
### Release v0.4.33
|
|
27
|
+
|
|
28
|
+
- Docs: clarify natural language date-field mapping for package search and document `content_recent` usage with example
|
|
29
|
+
- Files: `src/tools/package.ts`, `src/server.ts`, `src/worker.ts`, `package.json`, `package-lock.json`
|
|
30
|
+
|
|
5
31
|
### Release v0.4.32
|
|
6
32
|
|
|
7
33
|
- Workers: align browser-like headers for fetch path to avoid 403 on dati.gov.it
|
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
|
@@ -142,12 +142,49 @@ function asBuffer(data) {
|
|
|
142
142
|
}
|
|
143
143
|
return void 0;
|
|
144
144
|
}
|
|
145
|
+
function asArrayBuffer(data) {
|
|
146
|
+
if (!data) {
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
if (data instanceof ArrayBuffer) {
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
if (ArrayBuffer.isView(data)) {
|
|
153
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
154
|
+
}
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
async function decodeArrayBufferText(buffer, encoding) {
|
|
158
|
+
if (encoding && typeof DecompressionStream !== "undefined") {
|
|
159
|
+
try {
|
|
160
|
+
const stream = new DecompressionStream(
|
|
161
|
+
encoding.includes("br") ? "br" : encoding.includes("deflate") ? "deflate" : "gzip"
|
|
162
|
+
);
|
|
163
|
+
const decompressed = await new Response(
|
|
164
|
+
new Blob([buffer]).stream().pipeThrough(stream)
|
|
165
|
+
).arrayBuffer();
|
|
166
|
+
return new TextDecoder("utf-8").decode(decompressed).trim();
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return new TextDecoder("utf-8").decode(buffer).trim();
|
|
171
|
+
}
|
|
145
172
|
async function decodePossiblyCompressed(data, headers) {
|
|
146
173
|
if (data === null || data === void 0) {
|
|
147
174
|
return data;
|
|
148
175
|
}
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
const arrayBuffer = asArrayBuffer(data);
|
|
177
|
+
if (arrayBuffer && typeof Buffer === "undefined") {
|
|
178
|
+
const encoding2 = getHeaderValue(headers, "content-encoding");
|
|
179
|
+
const text2 = await decodeArrayBufferText(arrayBuffer, encoding2);
|
|
180
|
+
if (!text2) {
|
|
181
|
+
return text2;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(text2);
|
|
185
|
+
} catch {
|
|
186
|
+
return text2;
|
|
187
|
+
}
|
|
151
188
|
}
|
|
152
189
|
if (typeof data === "string") {
|
|
153
190
|
try {
|
|
@@ -158,6 +195,9 @@ async function decodePossiblyCompressed(data, headers) {
|
|
|
158
195
|
}
|
|
159
196
|
const buffer = asBuffer(data);
|
|
160
197
|
if (!buffer) {
|
|
198
|
+
if (typeof data === "object") {
|
|
199
|
+
return data;
|
|
200
|
+
}
|
|
161
201
|
return data;
|
|
162
202
|
}
|
|
163
203
|
const encoding = getHeaderValue(headers, "content-encoding");
|
|
@@ -241,7 +281,18 @@ async function makeCkanRequest(serverUrl, action, params = {}) {
|
|
|
241
281
|
method: "GET",
|
|
242
282
|
headers: {
|
|
243
283
|
Accept: "application/json, text/plain, */*",
|
|
244
|
-
"Accept-Language": "en-US,en;q=0.9,it;q=0.8"
|
|
284
|
+
"Accept-Language": "en-US,en;q=0.9,it;q=0.8",
|
|
285
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
286
|
+
Connection: "keep-alive",
|
|
287
|
+
Referer: `${baseUrl}/`,
|
|
288
|
+
"Sec-Fetch-Site": "same-origin",
|
|
289
|
+
"Sec-Fetch-Mode": "navigate",
|
|
290
|
+
"Sec-Fetch-Dest": "document",
|
|
291
|
+
"Upgrade-Insecure-Requests": "1",
|
|
292
|
+
"Sec-CH-UA": '"Chromium";v="120", "Not?A_Brand";v="24", "Google Chrome";v="120"',
|
|
293
|
+
"Sec-CH-UA-Mobile": "?0",
|
|
294
|
+
"Sec-CH-UA-Platform": '"Linux"',
|
|
295
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
245
296
|
}
|
|
246
297
|
});
|
|
247
298
|
if (!response.ok) {
|
|
@@ -668,6 +719,11 @@ Important - Date field semantics:
|
|
|
668
719
|
- metadata_modified: CKAN record update timestamp (publish time on source portals,
|
|
669
720
|
harvest time on aggregators; use for "updated/modified in last X")
|
|
670
721
|
|
|
722
|
+
Natural language mapping (important for tool callers):
|
|
723
|
+
- "created"/"published" -> prefer issued; fallback to metadata_created
|
|
724
|
+
- "updated"/"modified" -> prefer modified; fallback to metadata_modified
|
|
725
|
+
- For "recent in last X", consider using content_recent (issued with metadata_created fallback)
|
|
726
|
+
|
|
671
727
|
Content-recent helper:
|
|
672
728
|
- content_recent: if true, rewrites the query to use issued with a fallback to
|
|
673
729
|
metadata_created when issued is missing.
|
|
@@ -743,6 +799,7 @@ Examples:
|
|
|
743
799
|
- Date range: { q: "metadata_modified:[2024-01-01T00:00:00Z TO 2024-12-31T23:59:59Z]" }
|
|
744
800
|
- Date math: { q: "metadata_modified:[NOW-6MONTHS TO *]" }
|
|
745
801
|
- Date math (auto-converted): { q: "modified:[NOW-30DAYS TO NOW]" }
|
|
802
|
+
- Recent content (issued w/ fallback): { q: "*:*", content_recent: true, content_recent_days: 180 }
|
|
746
803
|
- Field exists: { q: "organization:* AND num_resources:[1 TO *]" }
|
|
747
804
|
- Boosting: { q: "title:climate^2 OR notes:climate" }
|
|
748
805
|
- Filter org: { fq: "organization:regione-siciliana" }
|
|
@@ -2401,6 +2458,47 @@ var DIMENSION_MAX = {
|
|
|
2401
2458
|
reusability: 75,
|
|
2402
2459
|
contextuality: 20
|
|
2403
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
|
+
};
|
|
2404
2502
|
function parseScoreValue(value) {
|
|
2405
2503
|
if (!value || typeof value !== "object") {
|
|
2406
2504
|
return void 0;
|
|
@@ -2415,6 +2513,44 @@ function parseScoreValue(value) {
|
|
|
2415
2513
|
}
|
|
2416
2514
|
return void 0;
|
|
2417
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
|
+
}
|
|
2418
2554
|
function decodeMetricsPayload(payload) {
|
|
2419
2555
|
if (payload && typeof payload === "object" && "@graph" in payload) {
|
|
2420
2556
|
return payload;
|
|
@@ -2487,6 +2623,86 @@ function extractMetricsScores(metricsData) {
|
|
|
2487
2623
|
}
|
|
2488
2624
|
return scores;
|
|
2489
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
|
+
}
|
|
2490
2706
|
function findNonMaxDimensions(scores) {
|
|
2491
2707
|
const nonMax = [];
|
|
2492
2708
|
Object.keys(DIMENSION_MAX).forEach((dimension) => {
|
|
@@ -2497,7 +2713,7 @@ function findNonMaxDimensions(scores) {
|
|
|
2497
2713
|
});
|
|
2498
2714
|
return nonMax;
|
|
2499
2715
|
}
|
|
2500
|
-
async function
|
|
2716
|
+
async function fetchMqaQuality(serverUrl, datasetId) {
|
|
2501
2717
|
const dataset = await makeCkanRequest(
|
|
2502
2718
|
serverUrl,
|
|
2503
2719
|
"package_show",
|
|
@@ -2551,6 +2767,7 @@ async function getMqaQuality(serverUrl, datasetId) {
|
|
|
2551
2767
|
};
|
|
2552
2768
|
return {
|
|
2553
2769
|
mqa: response.data,
|
|
2770
|
+
metrics: metricsPayload,
|
|
2554
2771
|
breakdown
|
|
2555
2772
|
};
|
|
2556
2773
|
} catch (error) {
|
|
@@ -2567,6 +2784,21 @@ async function getMqaQuality(serverUrl, datasetId) {
|
|
|
2567
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').`
|
|
2568
2785
|
);
|
|
2569
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
|
+
}
|
|
2570
2802
|
function findSectionMetric(section, key) {
|
|
2571
2803
|
if (!Array.isArray(section)) {
|
|
2572
2804
|
return void 0;
|
|
@@ -2761,7 +2993,57 @@ function formatQualityMarkdown(data, datasetId) {
|
|
|
2761
2993
|
const portalId = normalized.breakdown?.portalId || normalized.id || datasetId;
|
|
2762
2994
|
lines.push(`Portal: https://data.europa.eu/data/datasets/${portalId}/quality?locale=it`);
|
|
2763
2995
|
lines.push(`MQA source: ${MQA_API_BASE}/${portalId}`);
|
|
2764
|
-
|
|
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`}`);
|
|
2765
3047
|
return lines.join("\n");
|
|
2766
3048
|
}
|
|
2767
3049
|
function registerQualityTools(server2) {
|
|
@@ -2805,6 +3087,46 @@ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and on
|
|
|
2805
3087
|
}
|
|
2806
3088
|
}
|
|
2807
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
|
+
);
|
|
2808
3130
|
}
|
|
2809
3131
|
|
|
2810
3132
|
// src/resources/dataset.ts
|
|
@@ -3346,7 +3668,7 @@ var registerAllPrompts = (server2) => {
|
|
|
3346
3668
|
function createServer() {
|
|
3347
3669
|
return new McpServer({
|
|
3348
3670
|
name: "ckan-mcp-server",
|
|
3349
|
-
version: "0.4.
|
|
3671
|
+
version: "0.4.33"
|
|
3350
3672
|
});
|
|
3351
3673
|
}
|
|
3352
3674
|
function registerAll(server2) {
|