@aborruso/ckan-mcp-server 0.4.112 → 0.4.113
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 +43 -25
- package/dist/index.js +87 -58
- package/dist/worker.js +93 -96
- package/package.json +1 -1
package/LOG.md
CHANGED
|
@@ -1,47 +1,65 @@
|
|
|
1
1
|
# LOG
|
|
2
2
|
|
|
3
|
+
## 2026-07-31
|
|
4
|
+
|
|
5
|
+
### v0.4.113
|
|
6
|
+
|
|
7
|
+
JSON output always parseable (issue #39).
|
|
8
|
+
|
|
9
|
+
The docs promised "always valid JSON"; the code did not deliver it. Surfaced while reviewing PR #38 (an AI contribution by `averyquinnhq`), which documented the real behaviour and contradicted issue #37 — the PR was right.
|
|
10
|
+
|
|
11
|
+
- **Live repro**: `ckan_tag_list limit=1000` on dati.gov.it → 50,046 characters, `JSON.parse` fails. Not a corner case: an ordinary call.
|
|
12
|
+
- **`truncateJson`**: the last-resort branch cut the serialized string (`truncateText(JSON.stringify(...))`, with an in-code comment admitting "may produce invalid JSON"). It now empties arrays progressively and, when that is not enough, degrades to a valid `{_truncated, _error}`. `SHRINKABLE_KEYS` extended (+`rows`, `datasets`, `portals`, `facets`, `fields`) with a sacrifice order: bulk rows first, `fields` last.
|
|
13
|
+
- **8 tools + 4 Resources** used `truncateText(JSON.stringify(...))` directly: routed through `truncateJson`. `sparql_query` appended `/* output truncated */` to cut JSON (JSON has no comments): rewritten.
|
|
14
|
+
- **4 tools with no cap at all**: `ckan_get_mqa_quality`, `ckan_get_mqa_quality_details` (bare JSON.stringify), `ckan_find_portals`, `ckan_status_show` (markdown).
|
|
15
|
+
- **A third unparseable path**: no `catch` honoured `response_format` — every error came back as prose even with `json`. New `formatError()`; errors now return `{error, _error: true}` plus `isError: true`. Zod validation errors stay textual (emitted by the SDK before the handler runs).
|
|
16
|
+
- **`structuredContent` stays uncapped** (65,382 characters against 50,000 of text on tag_list): capping it would drop rows from `datastore-table-ui`, which consumes it. Decision deferred, documented in `docs/DECISIONS.md`.
|
|
17
|
+
- 10 new tests (`truncateJson` had none), 453 passing. E2e: `tag_list` from PARSE-FAIL to parse-ok; real errors parseable in json mode.
|
|
18
|
+
- `CONTRIBUTING.md`: section on AI-assisted contributions (disclosure, small diffs, verifiable claims).
|
|
19
|
+
- Review follow-up: `addDemoFooter()` was appended to JSON output in `quality.ts`, breaking parsing on Workers — now wraps the Markdown branch only, inside `truncateText`. Added `isError: true` to the two non-dati.gov.it guards. The `truncateJson` fallback now degrades further so it always respects small limits. 454 passing.
|
|
20
|
+
|
|
3
21
|
## 2026-07-09
|
|
4
22
|
|
|
5
23
|
### v0.4.112
|
|
6
24
|
|
|
7
|
-
Security —
|
|
25
|
+
Security — Round 3: hardening (closes the last group of advisories in triage).
|
|
8
26
|
|
|
9
|
-
- **Error reflection**: `makeCkanRequest`
|
|
10
|
-
- **postMessage UI** (`resources/datastore-table-ui.ts`):
|
|
11
|
-
- **Prompt
|
|
12
|
-
- 3
|
|
27
|
+
- **Error reflection**: `makeCkanRequest` no longer embeds the upstream body (`JSON.stringify(decodedData)`) in the error returned to the caller — now a generic action-scoped message, with detail on stderr only (truncated). `worker.ts` catch-all: removed `error.message` from the JSON-RPC `data` field (server-side logging only). Closes the semi-blind read channel of the SSRF.
|
|
28
|
+
- **postMessage UI** (`resources/datastore-table-ui.ts`): the host origin is pinned from the reply to the `ui/initialize` handshake; messages carrying data are accepted only from that origin, and outbound messages use an explicit target origin (never `'*'`).
|
|
29
|
+
- **Prompt injection on org/group**: extended the c499 containment to the `organization.ts` and `group.ts` renderers — `description` in an untrusted block (`wrapUntrusted`), newlines collapsed in lists.
|
|
30
|
+
- 3 new tests (error no-leak, org/group description fencing); 443 passing. E2e: generic error on 404 (no internal body), normal requests fine. Worker build fine.
|
|
13
31
|
|
|
14
32
|
### v0.4.111
|
|
15
33
|
|
|
16
|
-
Security —
|
|
34
|
+
Security — Round 2: three distinct low-cost bugs.
|
|
17
35
|
|
|
18
|
-
- **MQA allowlist bypass** (`isValidMqaServer`, `tools/quality.ts`): regex
|
|
19
|
-
- **Decompression bomb / unbounded buffering** (`utils/http.ts`):
|
|
20
|
-
- **Cache-key collision** (`utils/cache.ts`): `canonicalizeParams`
|
|
21
|
-
- 2
|
|
22
|
-
-
|
|
36
|
+
- **MQA allowlist bypass** (`isValidMqaServer`, `tools/quality.ts`): unanchored regex replaced by URL parsing plus exact host comparison (`dati.gov.it`/`www.dati.gov.it`). Suffix (`dati.gov.it.attacker.com`) and userinfo (`dati.gov.it@attacker.com`) tricks are now rejected.
|
|
37
|
+
- **Decompression bomb / unbounded buffering** (`utils/http.ts`): response size cap (`maxContentLength`/`maxBodyLength` on axios plus a byte check on `arrayBuffer` in the fetch branch, default 32MB) and decompression output cap (`maxOutputLength` on gunzip/brotli/inflateSync plus a check on DecompressionStream, default 64MB). Override via `CKAN_MAX_RESPONSE_BYTES`/`CKAN_MAX_DECOMPRESSED_BYTES`. Stops OOM/stalls from hyper-compressed payloads.
|
|
38
|
+
- **Cache-key collision** (`utils/cache.ts`): `canonicalizeParams` now produces typed canonical JSON (recursive sort) instead of a `k=v` join with unescaped `&`; `buildCacheKey` frames it in `JSON.stringify([url,action,canon])`. `{q:"budget",rows:10}` and `{q:"budget&rows=10"}` no longer collide.
|
|
39
|
+
- 2 new tests (plus updated regressions); 440 passing. E2e: valid MQA host reaches data.europa.eu, bypass rejected, normal requests fine. Worker build fine.
|
|
40
|
+
- Further hardening in progress for upcoming releases.
|
|
23
41
|
|
|
24
42
|
### v0.4.110
|
|
25
43
|
|
|
26
|
-
Security —
|
|
44
|
+
Security — Round 1: SSRF cluster on the `fetch` path (GHSA-vmrr, GHSA-38f8; GHSA-8hxx clarified):
|
|
27
45
|
|
|
28
|
-
-
|
|
29
|
-
- **`assertHostnameResolvesSafe`
|
|
30
|
-
- **GHSA-8hxx**:
|
|
31
|
-
- MQA fetch: host
|
|
32
|
-
- 6
|
|
33
|
-
-
|
|
46
|
+
- **Centralized `safeFetch()`** in `utils/http.ts`: `redirect:"manual"` plus re-validation of every hop (`validateServerUrl` + `assertHostnameResolvesSafe`), bounded hops, `httpsOnly` option. Closes redirect-SSRF (e.g. a public endpoint 302-ing to `169.254.169.254`) without breaking legitimate canonical redirects. Used by `sparql_query` (3 fetches) and by the MQA metrics fetch in `quality.ts`.
|
|
47
|
+
- **`assertHostnameResolvesSafe` is now fail-closed**: it distinguishes "DNS module absent (Workers) → no-op" from "resolution failed → throw". Previously a DNS error let the request proceed (fail-open / TOCTOU).
|
|
48
|
+
- **GHSA-8hxx**: verified that WHATWG `URL` already normalizes IPv4 encodings (int/hex/octal/short) to dotted-decimal before `validateServerUrl` → the existing check already covers them. No code added (the advisory PoC tested the regex against raw strings, not the parsed hostname). Only a comment and regression tests. The remaining `::7f00:1` (IPv4-compatible IPv6) is not routable, as the advisory itself concedes.
|
|
49
|
+
- MQA fetch: constant host (`data.europa.eu`), so hardening for consistency rather than a real vector.
|
|
50
|
+
- 6 new tests (redirect→internal blocked, redirect→non-HTTPS rejected, fail-closed on DNS error, IPv4 encodings blocked). 438 passing. E2e: Wikidata via `safeFetch` fine, internal IP blocked. Worker build fine.
|
|
51
|
+
- Further security hardening in progress for upcoming releases.
|
|
34
52
|
|
|
35
53
|
### v0.4.109
|
|
36
54
|
|
|
37
|
-
Security hardening — 3 advisories, "
|
|
55
|
+
Security hardening — 3 advisories, "poison and door" (environmental risk before the knives):
|
|
38
56
|
|
|
39
|
-
- **GHSA-3369** (second-order SSRF, `ckan_list_resources`): source-portal probing
|
|
40
|
-
- **GHSA-c499** (indirect prompt injection):
|
|
41
|
-
- **GHSA-v3j5** (HTTP transport
|
|
42
|
-
- 4
|
|
43
|
-
- Docs: README (
|
|
44
|
-
- **
|
|
57
|
+
- **GHSA-3369** (second-order SSRF, `ckan_list_resources`): source-portal probing is now **opt-in** (`check_source_portal` defaults to `false`). It used to be ON: listing a dataset's resources contacted hosts and ports taken from the dataset's own data (confused deputy + port-scan oracle + amplification via `Promise.all`). Added: ports other than 80/443 dropped in `extractSourcePortal` (uses `hostname`, not `host`), fan-out capped at 10 probes.
|
|
58
|
+
- **GHSA-c499** (indirect prompt injection): free-text portal fields (`notes`, resource `description`) were rendered verbatim in the output. They are now wrapped in a delimited `untrusted` block with a warning (`wrapUntrusted`), with inner fences neutralized; portal URLs are scheme-validated (http/https only) and rendered as inline code (`safeUrlText`); `ckan_list_resources` table cells are neutralized (`|`, newlines). Containment, not a complete fix — documented for integrators.
|
|
59
|
+
- **GHSA-v3j5** (exposed HTTP transport): binds to **`127.0.0.1`** by default (was `0.0.0.0`), `enableDnsRebindingProtection` plus `allowedHosts`/`allowedOrigins`. `docker-compose.yml` publishes on `127.0.0.1:3000:3000` (with `CKAN_HTTP_HOST=0.0.0.0` inside the container). New env vars: `CKAN_HTTP_HOST`, `CKAN_HTTP_ALLOWED_HOSTS`, `CKAN_HTTP_ALLOWED_ORIGINS`. Closing this door downgrades the whole SSRF cluster from "remote" to "local".
|
|
60
|
+
- 4 new tests; 432 passing. Verified e2e against a real HTTP deployment (loopback bind, 403 on disallowed Host, no probing by default, notes fenced). Worker build fine.
|
|
61
|
+
- Docs: README (HTTP env var table), SKILL (source-portal now opt-in), docker/README, docker-compose.
|
|
62
|
+
- **Not done yet** (fetch SSRF cluster, MQA regex, cache, decompression bomb, postMessage UI): knives and hardening, in later rounds.
|
|
45
63
|
|
|
46
64
|
## 2026-06-22
|
|
47
65
|
|
package/dist/index.js
CHANGED
|
@@ -1009,26 +1009,48 @@ function truncateText(text, limit = CHARACTER_LIMIT) {
|
|
|
1009
1009
|
|
|
1010
1010
|
... [Response truncated at ${limit} characters]`;
|
|
1011
1011
|
}
|
|
1012
|
+
var SHRINKABLE_KEYS = [
|
|
1013
|
+
"results",
|
|
1014
|
+
"records",
|
|
1015
|
+
"rows",
|
|
1016
|
+
"datasets",
|
|
1017
|
+
"resources",
|
|
1018
|
+
"packages",
|
|
1019
|
+
"organizations",
|
|
1020
|
+
"groups",
|
|
1021
|
+
"portals",
|
|
1022
|
+
"tags",
|
|
1023
|
+
"facets",
|
|
1024
|
+
"fields"
|
|
1025
|
+
];
|
|
1012
1026
|
function truncateJson(obj, limit = CHARACTER_LIMIT) {
|
|
1013
1027
|
let json = JSON.stringify(obj, null, 2);
|
|
1014
1028
|
if (json.length <= limit) return json;
|
|
1015
1029
|
json = JSON.stringify(obj);
|
|
1016
1030
|
if (json.length <= limit) return json;
|
|
1017
1031
|
const data = structuredClone(obj);
|
|
1018
|
-
const
|
|
1019
|
-
for (const key of arrayKeys) {
|
|
1032
|
+
for (const key of SHRINKABLE_KEYS) {
|
|
1020
1033
|
if (Array.isArray(data[key]) && data[key].length > 0) {
|
|
1021
1034
|
const originalCount = data[key].length;
|
|
1022
|
-
|
|
1035
|
+
data["_truncated"] = true;
|
|
1036
|
+
data["_original_count"] = originalCount;
|
|
1037
|
+
while (data[key].length > 0) {
|
|
1023
1038
|
data[key].pop();
|
|
1024
|
-
data["_truncated"] = true;
|
|
1025
|
-
data["_original_count"] = originalCount;
|
|
1026
1039
|
json = JSON.stringify(data);
|
|
1027
1040
|
if (json.length <= limit) return json;
|
|
1028
1041
|
}
|
|
1029
1042
|
}
|
|
1030
1043
|
}
|
|
1031
|
-
|
|
1044
|
+
const explained = JSON.stringify({
|
|
1045
|
+
_truncated: true,
|
|
1046
|
+
_error: `Response exceeded the ${limit} character limit and could not be reduced to fit. Narrow the query or use pagination.`
|
|
1047
|
+
});
|
|
1048
|
+
if (explained.length <= limit) return explained;
|
|
1049
|
+
const bare = JSON.stringify({ _truncated: true });
|
|
1050
|
+
return bare.length <= limit ? bare : "{}";
|
|
1051
|
+
}
|
|
1052
|
+
function formatError(message, asJson) {
|
|
1053
|
+
return asJson ? JSON.stringify({ error: message, _error: true }, null, 2) : message;
|
|
1032
1054
|
}
|
|
1033
1055
|
function wrapUntrusted(text) {
|
|
1034
1056
|
const safe = String(text).replace(/```/g, "\u02BC\u02BC\u02BC");
|
|
@@ -2016,7 +2038,7 @@ Note: showing top ${sorted.length} only. Use \`response_format: json\` for full
|
|
|
2016
2038
|
};
|
|
2017
2039
|
} catch (error) {
|
|
2018
2040
|
return {
|
|
2019
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_package_search") }],
|
|
2041
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_package_search"), params.response_format === "json" /* JSON */) }],
|
|
2020
2042
|
isError: true
|
|
2021
2043
|
};
|
|
2022
2044
|
}
|
|
@@ -2137,7 +2159,7 @@ Typical workflow: ckan_find_relevant_datasets \u2192 ckan_package_show (inspect
|
|
|
2137
2159
|
};
|
|
2138
2160
|
if (params.response_format === "json" /* JSON */) {
|
|
2139
2161
|
return {
|
|
2140
|
-
content: [{ type: "text", text:
|
|
2162
|
+
content: [{ type: "text", text: truncateJson(payload) }],
|
|
2141
2163
|
structuredContent: payload
|
|
2142
2164
|
};
|
|
2143
2165
|
}
|
|
@@ -2218,7 +2240,7 @@ Typical workflow: ckan_find_relevant_datasets \u2192 ckan_package_show (inspect
|
|
|
2218
2240
|
};
|
|
2219
2241
|
} catch (error) {
|
|
2220
2242
|
return {
|
|
2221
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_find_relevant_datasets") }],
|
|
2243
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_find_relevant_datasets"), params.response_format === "json" /* JSON */) }],
|
|
2222
2244
|
isError: true
|
|
2223
2245
|
};
|
|
2224
2246
|
}
|
|
@@ -2294,7 +2316,7 @@ Typical workflow: ckan_package_show \u2192 pick a resource with datastore_active
|
|
|
2294
2316
|
};
|
|
2295
2317
|
} catch (error) {
|
|
2296
2318
|
return {
|
|
2297
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_package_show") }],
|
|
2319
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_package_show"), params.response_format === "json" /* JSON */) }],
|
|
2298
2320
|
isError: true
|
|
2299
2321
|
};
|
|
2300
2322
|
}
|
|
@@ -2396,7 +2418,7 @@ dataset's own resource URLs); set check_source_portal=true to enable it.`,
|
|
|
2396
2418
|
resources: summary
|
|
2397
2419
|
};
|
|
2398
2420
|
return {
|
|
2399
|
-
content: [{ type: "text", text:
|
|
2421
|
+
content: [{ type: "text", text: truncateJson(payload) }],
|
|
2400
2422
|
structuredContent: payload
|
|
2401
2423
|
};
|
|
2402
2424
|
}
|
|
@@ -2459,7 +2481,7 @@ dataset's own resource URLs); set check_source_portal=true to enable it.`,
|
|
|
2459
2481
|
};
|
|
2460
2482
|
} catch (error) {
|
|
2461
2483
|
return {
|
|
2462
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_list_resources") }],
|
|
2484
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_list_resources"), params.response_format === "json" /* JSON */) }],
|
|
2463
2485
|
isError: true
|
|
2464
2486
|
};
|
|
2465
2487
|
}
|
|
@@ -2746,7 +2768,7 @@ Note: organization_list returned 500; using package_search facets.
|
|
|
2746
2768
|
};
|
|
2747
2769
|
} catch (error) {
|
|
2748
2770
|
return {
|
|
2749
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_organization_list") }],
|
|
2771
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_organization_list"), params.response_format === "json" /* JSON */) }],
|
|
2750
2772
|
isError: true
|
|
2751
2773
|
};
|
|
2752
2774
|
}
|
|
@@ -2807,7 +2829,7 @@ Typical workflow: ckan_organization_show \u2192 ckan_package_show (inspect a dat
|
|
|
2807
2829
|
};
|
|
2808
2830
|
} catch (error) {
|
|
2809
2831
|
return {
|
|
2810
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_organization_show") }],
|
|
2832
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_organization_show"), params.response_format === "json" /* JSON */) }],
|
|
2811
2833
|
isError: true
|
|
2812
2834
|
};
|
|
2813
2835
|
}
|
|
@@ -2874,7 +2896,7 @@ Typical workflow: ckan_organization_search \u2192 ckan_organization_show (get de
|
|
|
2874
2896
|
}))
|
|
2875
2897
|
};
|
|
2876
2898
|
return {
|
|
2877
|
-
content: [{ type: "text", text:
|
|
2899
|
+
content: [{ type: "text", text: truncateJson(jsonResult) }],
|
|
2878
2900
|
structuredContent: jsonResult
|
|
2879
2901
|
};
|
|
2880
2902
|
}
|
|
@@ -2915,7 +2937,7 @@ Typical workflow: ckan_organization_search \u2192 ckan_organization_show (get de
|
|
|
2915
2937
|
};
|
|
2916
2938
|
} catch (error) {
|
|
2917
2939
|
return {
|
|
2918
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_organization_search") }],
|
|
2940
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_organization_search"), params.response_format === "json" /* JSON */) }],
|
|
2919
2941
|
isError: true
|
|
2920
2942
|
};
|
|
2921
2943
|
}
|
|
@@ -3127,7 +3149,7 @@ Typical workflow: ckan_package_search \u2192 ckan_package_show (find resource_id
|
|
|
3127
3149
|
};
|
|
3128
3150
|
} catch (error) {
|
|
3129
3151
|
return {
|
|
3130
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_datastore_search") }],
|
|
3152
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_datastore_search"), params.response_format === "json" /* JSON */) }],
|
|
3131
3153
|
isError: true
|
|
3132
3154
|
};
|
|
3133
3155
|
}
|
|
@@ -3188,7 +3210,7 @@ Security note: SQL queries are forwarded directly to the CKAN DataStore API. The
|
|
|
3188
3210
|
};
|
|
3189
3211
|
} catch (error) {
|
|
3190
3212
|
return {
|
|
3191
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_datastore_search_sql") }],
|
|
3213
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_datastore_search_sql"), params.response_format === "json" /* JSON */) }],
|
|
3192
3214
|
isError: true
|
|
3193
3215
|
};
|
|
3194
3216
|
}
|
|
@@ -3251,7 +3273,7 @@ Typical workflow: ckan_status_show (verify server is up) \u2192 ckan_package_sea
|
|
|
3251
3273
|
]);
|
|
3252
3274
|
const markdown = formatStatusMarkdown(result, params.server_url, hvdCount);
|
|
3253
3275
|
return {
|
|
3254
|
-
content: [{ type: "text", text: addDemoFooter(markdown) }],
|
|
3276
|
+
content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }],
|
|
3255
3277
|
structuredContent: result
|
|
3256
3278
|
};
|
|
3257
3279
|
} catch (error) {
|
|
@@ -3363,7 +3385,7 @@ Typical workflow: ckan_tag_list \u2192 ckan_package_search with fq="tags:tag_nam
|
|
|
3363
3385
|
tags
|
|
3364
3386
|
};
|
|
3365
3387
|
return {
|
|
3366
|
-
content: [{ type: "text", text:
|
|
3388
|
+
content: [{ type: "text", text: truncateJson(output) }],
|
|
3367
3389
|
structuredContent: output
|
|
3368
3390
|
};
|
|
3369
3391
|
}
|
|
@@ -3397,7 +3419,7 @@ Typical workflow: ckan_tag_list \u2192 ckan_package_search with fq="tags:tag_nam
|
|
|
3397
3419
|
return {
|
|
3398
3420
|
content: [{
|
|
3399
3421
|
type: "text",
|
|
3400
|
-
text: `Error listing tags: ${error instanceof Error ? error.message : String(error)}
|
|
3422
|
+
text: formatError(`Error listing tags: ${error instanceof Error ? error.message : String(error)}`, params.response_format === "json" /* JSON */)
|
|
3401
3423
|
}],
|
|
3402
3424
|
isError: true
|
|
3403
3425
|
};
|
|
@@ -3640,7 +3662,7 @@ Typical workflow: ckan_group_list \u2192 ckan_group_show (inspect one) \u2192 ck
|
|
|
3640
3662
|
};
|
|
3641
3663
|
} catch (error) {
|
|
3642
3664
|
return {
|
|
3643
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_group_list") }],
|
|
3665
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_group_list"), params.response_format === "json" /* JSON */) }],
|
|
3644
3666
|
isError: true
|
|
3645
3667
|
};
|
|
3646
3668
|
}
|
|
@@ -3698,7 +3720,7 @@ Typical workflow: ckan_group_show \u2192 ckan_package_show (inspect a dataset) \
|
|
|
3698
3720
|
};
|
|
3699
3721
|
} catch (error) {
|
|
3700
3722
|
return {
|
|
3701
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_group_show") }],
|
|
3723
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_group_show"), params.response_format === "json" /* JSON */) }],
|
|
3702
3724
|
isError: true
|
|
3703
3725
|
};
|
|
3704
3726
|
}
|
|
@@ -3760,7 +3782,7 @@ Typical workflow: ckan_group_search \u2192 ckan_group_show (get details) \u2192
|
|
|
3760
3782
|
}))
|
|
3761
3783
|
};
|
|
3762
3784
|
return {
|
|
3763
|
-
content: [{ type: "text", text:
|
|
3785
|
+
content: [{ type: "text", text: truncateJson(jsonResult) }],
|
|
3764
3786
|
structuredContent: jsonResult
|
|
3765
3787
|
};
|
|
3766
3788
|
}
|
|
@@ -3800,7 +3822,7 @@ Typical workflow: ckan_group_search \u2192 ckan_group_show (get details) \u2192
|
|
|
3800
3822
|
};
|
|
3801
3823
|
} catch (error) {
|
|
3802
3824
|
return {
|
|
3803
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_group_search") }],
|
|
3825
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_group_search"), params.response_format === "json" /* JSON */) }],
|
|
3804
3826
|
isError: true
|
|
3805
3827
|
};
|
|
3806
3828
|
}
|
|
@@ -4457,25 +4479,31 @@ function registerQualityTools(server) {
|
|
|
4457
4479
|
return {
|
|
4458
4480
|
content: [{
|
|
4459
4481
|
type: "text",
|
|
4460
|
-
text:
|
|
4482
|
+
text: formatError(
|
|
4483
|
+
`Error: MQA quality metrics are only available for dati.gov.it datasets. Provided server: ${server_url}
|
|
4461
4484
|
|
|
4462
|
-
The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and only evaluates datasets from Italian open data portal
|
|
4463
|
-
|
|
4485
|
+
The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and only evaluates datasets from Italian open data portal.`,
|
|
4486
|
+
response_format === "json" /* JSON */
|
|
4487
|
+
)
|
|
4488
|
+
}],
|
|
4489
|
+
isError: true
|
|
4464
4490
|
};
|
|
4465
4491
|
}
|
|
4466
4492
|
try {
|
|
4467
4493
|
const qualityData = await getMqaQuality(server_url, dataset_id);
|
|
4468
4494
|
const format = response_format || "markdown" /* MARKDOWN */;
|
|
4469
|
-
const output = format === "json" /* JSON */ ?
|
|
4495
|
+
const output = format === "json" /* JSON */ ? truncateJson(qualityData) : truncateText(addDemoFooter(formatQualityMarkdown(qualityData, dataset_id)));
|
|
4470
4496
|
return {
|
|
4471
4497
|
content: [{
|
|
4472
4498
|
type: "text",
|
|
4473
|
-
text:
|
|
4499
|
+
text: output
|
|
4474
4500
|
}]
|
|
4475
4501
|
};
|
|
4476
4502
|
} catch (error) {
|
|
4503
|
+
const message = `Error retrieving quality metrics: ${formatCkanError(error, "ckan_get_mqa_quality")}`;
|
|
4477
4504
|
return {
|
|
4478
|
-
content: [{ type: "text", text:
|
|
4505
|
+
content: [{ type: "text", text: formatError(message, response_format === "json" /* JSON */) }],
|
|
4506
|
+
isError: true
|
|
4479
4507
|
};
|
|
4480
4508
|
}
|
|
4481
4509
|
}
|
|
@@ -4502,25 +4530,31 @@ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and on
|
|
|
4502
4530
|
return {
|
|
4503
4531
|
content: [{
|
|
4504
4532
|
type: "text",
|
|
4505
|
-
text:
|
|
4533
|
+
text: formatError(
|
|
4534
|
+
`Error: MQA quality details are only available for dati.gov.it datasets. Provided server: ${server_url}
|
|
4506
4535
|
|
|
4507
|
-
The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and only evaluates datasets from Italian open data portal
|
|
4508
|
-
|
|
4536
|
+
The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and only evaluates datasets from Italian open data portal.`,
|
|
4537
|
+
response_format === "json" /* JSON */
|
|
4538
|
+
)
|
|
4539
|
+
}],
|
|
4540
|
+
isError: true
|
|
4509
4541
|
};
|
|
4510
4542
|
}
|
|
4511
4543
|
try {
|
|
4512
4544
|
const details = await getMqaQualityDetails(server_url, dataset_id);
|
|
4513
4545
|
const format = response_format || "markdown" /* MARKDOWN */;
|
|
4514
|
-
const output = format === "json" /* JSON */ ?
|
|
4546
|
+
const output = format === "json" /* JSON */ ? truncateJson(details) : truncateText(addDemoFooter(formatQualityDetailsMarkdown(details, dataset_id)));
|
|
4515
4547
|
return {
|
|
4516
4548
|
content: [{
|
|
4517
4549
|
type: "text",
|
|
4518
|
-
text:
|
|
4550
|
+
text: output
|
|
4519
4551
|
}]
|
|
4520
4552
|
};
|
|
4521
4553
|
} catch (error) {
|
|
4554
|
+
const message = `Error retrieving quality details: ${formatCkanError(error, "ckan_get_mqa_quality_details")}`;
|
|
4522
4555
|
return {
|
|
4523
|
-
content: [{ type: "text", text:
|
|
4556
|
+
content: [{ type: "text", text: formatError(message, response_format === "json" /* JSON */) }],
|
|
4557
|
+
isError: true
|
|
4524
4558
|
};
|
|
4525
4559
|
}
|
|
4526
4560
|
}
|
|
@@ -4686,7 +4720,7 @@ Typical workflow: ckan_analyze_datasets \u2192 ckan_datastore_search (with known
|
|
|
4686
4720
|
}));
|
|
4687
4721
|
if (params.response_format === "json" /* JSON */) {
|
|
4688
4722
|
return {
|
|
4689
|
-
content: [{ type: "text", text:
|
|
4723
|
+
content: [{ type: "text", text: truncateJson({ total: searchResult.count, datasets: analyzed }) }]
|
|
4690
4724
|
};
|
|
4691
4725
|
}
|
|
4692
4726
|
const markdown = formatAnalyzeDatasetsMarkdown(
|
|
@@ -4700,7 +4734,7 @@ Typical workflow: ckan_analyze_datasets \u2192 ckan_datastore_search (with known
|
|
|
4700
4734
|
};
|
|
4701
4735
|
} catch (error) {
|
|
4702
4736
|
return {
|
|
4703
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_analyze_datasets") }],
|
|
4737
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_analyze_datasets"), params.response_format === "json" /* JSON */) }],
|
|
4704
4738
|
isError: true
|
|
4705
4739
|
};
|
|
4706
4740
|
}
|
|
@@ -4779,7 +4813,7 @@ Typical workflow: ckan_catalog_stats (understand the portal) \u2192 ckan_package
|
|
|
4779
4813
|
);
|
|
4780
4814
|
if (params.response_format === "json" /* JSON */) {
|
|
4781
4815
|
return {
|
|
4782
|
-
content: [{ type: "text", text:
|
|
4816
|
+
content: [{ type: "text", text: truncateJson({ total: result.count, facets: result.facets }) }]
|
|
4783
4817
|
};
|
|
4784
4818
|
}
|
|
4785
4819
|
const markdown = formatCatalogStatsMarkdown(params.server_url, result.count, result.facets);
|
|
@@ -4788,7 +4822,7 @@ Typical workflow: ckan_catalog_stats (understand the portal) \u2192 ckan_package
|
|
|
4788
4822
|
};
|
|
4789
4823
|
} catch (error) {
|
|
4790
4824
|
return {
|
|
4791
|
-
content: [{ type: "text", text: formatCkanError(error, "ckan_catalog_stats") }],
|
|
4825
|
+
content: [{ type: "text", text: formatError(formatCkanError(error, "ckan_catalog_stats"), params.response_format === "json" /* JSON */) }],
|
|
4792
4826
|
isError: true
|
|
4793
4827
|
};
|
|
4794
4828
|
}
|
|
@@ -4952,26 +4986,21 @@ Typical workflow: sparql_query (explore schema) \u2192 sparql_query (targeted qu
|
|
|
4952
4986
|
const data = await querySparqlEndpoint(params.endpoint_url, limitedQuery);
|
|
4953
4987
|
if (params.response_format === "json" /* JSON */) {
|
|
4954
4988
|
const result = formatSparqlJson(data);
|
|
4955
|
-
let text2 = JSON.stringify(result, null, 2);
|
|
4956
|
-
if (text2.length > CHARACTER_LIMIT) {
|
|
4957
|
-
text2 = text2.slice(0, CHARACTER_LIMIT) + "\n/* output truncated */";
|
|
4958
|
-
}
|
|
4959
4989
|
return {
|
|
4960
|
-
content: [{ type: "text", text:
|
|
4990
|
+
content: [{ type: "text", text: truncateJson(result) }],
|
|
4961
4991
|
structuredContent: result
|
|
4962
4992
|
};
|
|
4963
4993
|
}
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
}
|
|
4968
|
-
return { content: [{ type: "text", text }] };
|
|
4994
|
+
return {
|
|
4995
|
+
content: [{ type: "text", text: truncateText(formatSparqlMarkdown(data, params.endpoint_url)) }]
|
|
4996
|
+
};
|
|
4969
4997
|
} catch (error) {
|
|
4998
|
+
const message = `SPARQL query failed:
|
|
4999
|
+
${error instanceof Error ? error.message : String(error)}`;
|
|
4970
5000
|
return {
|
|
4971
5001
|
content: [{
|
|
4972
5002
|
type: "text",
|
|
4973
|
-
text:
|
|
4974
|
-
${error instanceof Error ? error.message : String(error)}`
|
|
5003
|
+
text: formatError(message, params.response_format === "json" /* JSON */)
|
|
4975
5004
|
}],
|
|
4976
5005
|
isError: true
|
|
4977
5006
|
};
|
|
@@ -5081,7 +5110,7 @@ Typical workflow: ckan_find_portals (discover portal URL) \u2192 ckan_status_sho
|
|
|
5081
5110
|
});
|
|
5082
5111
|
const markdown = formatMarkdown(results, active.length, params.limit);
|
|
5083
5112
|
return {
|
|
5084
|
-
content: [{ type: "text", text: addDemoFooter(markdown) }],
|
|
5113
|
+
content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }],
|
|
5085
5114
|
structuredContent: { portals: results.map((p) => ({
|
|
5086
5115
|
url: p.Href,
|
|
5087
5116
|
title: p.SiteInfo.site_title,
|
|
@@ -5144,7 +5173,7 @@ function registerDatasetResource(server) {
|
|
|
5144
5173
|
const result = await makeCkanRequest(serverUrl, "package_show", {
|
|
5145
5174
|
id
|
|
5146
5175
|
});
|
|
5147
|
-
const content =
|
|
5176
|
+
const content = truncateJson(result);
|
|
5148
5177
|
return {
|
|
5149
5178
|
contents: [
|
|
5150
5179
|
{
|
|
@@ -5188,7 +5217,7 @@ function registerResourceResource(server) {
|
|
|
5188
5217
|
const result = await makeCkanRequest(serverUrl, "resource_show", {
|
|
5189
5218
|
id
|
|
5190
5219
|
});
|
|
5191
|
-
const content =
|
|
5220
|
+
const content = truncateJson(result);
|
|
5192
5221
|
return {
|
|
5193
5222
|
contents: [
|
|
5194
5223
|
{
|
|
@@ -5239,7 +5268,7 @@ function registerOrganizationResource(server) {
|
|
|
5239
5268
|
include_datasets: false
|
|
5240
5269
|
}
|
|
5241
5270
|
);
|
|
5242
|
-
const content =
|
|
5271
|
+
const content = truncateJson(result);
|
|
5243
5272
|
return {
|
|
5244
5273
|
contents: [
|
|
5245
5274
|
{
|
|
@@ -5298,7 +5327,7 @@ var registerDatasetFilterResource = (server, config) => {
|
|
|
5298
5327
|
q: "*:*",
|
|
5299
5328
|
fq
|
|
5300
5329
|
});
|
|
5301
|
-
const content =
|
|
5330
|
+
const content = truncateJson(result);
|
|
5302
5331
|
return {
|
|
5303
5332
|
contents: [
|
|
5304
5333
|
{
|