@aborruso/ckan-mcp-server 0.4.128 → 0.4.129
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 +56 -15
- package/package.json +1 -1
package/LOG.md
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# LOG
|
|
2
2
|
|
|
3
|
+
## 2026-10-06
|
|
4
|
+
|
|
5
|
+
### v0.4.129
|
|
6
|
+
|
|
7
|
+
Ships #558 (by @paoValle, closes #31): error messages now tell the caller what to try next where they said nothing. A timeout, DNS failure or network error gets "Portal unreachable. Retry later, or call `ckan_status_show`" (noting its answer can be cached up to an hour), on both Node and Workers; 5xx errors point at `ckan_status_show` too; a 404 on `ckan_group_show` suggests `ckan_group_list`/`ckan_group_search`. A request refused by the SSRF guard keeps the guard's own message instead of a retry hint. Released the same day as the merge, by explicit exception to the release cadence rule. Tests 587 passed, smoke 17/17.
|
|
8
|
+
|
|
3
9
|
## 2026-09-28
|
|
4
10
|
|
|
11
|
+
### Advisory GHSA-279h-fmcr-4rwv published
|
|
12
|
+
|
|
13
|
+
Reported by @manus-pi, accepted and published as Medium (CVSS 5.8): affected `<= 0.4.127`, patched `0.4.128`. CVE requested, not yet assigned. v0.4.128 is live on npm (with provenance), the MCP Registry and the Worker. `SECURITY.md` table updated; GHSA-x32r-mh7g-q2rf got CVE-2026-92146 in the meantime.
|
|
14
|
+
|
|
5
15
|
### v0.4.128
|
|
6
16
|
|
|
7
17
|
Ships #556: every redirect hop in `makeCkanRequest` is re-validated (GHSA-279h-fmcr-4rwv), on both the Node/axios and Workers/fetch paths. Smoke 17/17.
|
package/dist/index.js
CHANGED
|
@@ -476,7 +476,36 @@ var CkanApiError = class extends Error {
|
|
|
476
476
|
this.serverUrl = serverUrl;
|
|
477
477
|
}
|
|
478
478
|
};
|
|
479
|
+
var CkanTransportError = class extends Error {
|
|
480
|
+
/** The portal the request went to, as the caller wrote it. */
|
|
481
|
+
serverUrl;
|
|
482
|
+
constructor(message, serverUrl) {
|
|
483
|
+
super(message);
|
|
484
|
+
this.name = "CkanTransportError";
|
|
485
|
+
this.serverUrl = serverUrl;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var SsrfGuardError = class extends Error {
|
|
489
|
+
constructor(message) {
|
|
490
|
+
super(message);
|
|
491
|
+
this.name = "SsrfGuardError";
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
function unwrapSsrfGuardError(axiosError) {
|
|
495
|
+
if (axiosError.cause instanceof SsrfGuardError) return axiosError.cause;
|
|
496
|
+
const redirected = axiosError.cause?.cause;
|
|
497
|
+
return redirected instanceof SsrfGuardError ? redirected : null;
|
|
498
|
+
}
|
|
479
499
|
function formatCkanError(error, _toolName) {
|
|
500
|
+
if (error instanceof CkanTransportError) {
|
|
501
|
+
const migration2 = error.serverUrl ? getPortalMigration(error.serverUrl) : null;
|
|
502
|
+
if (migration2) {
|
|
503
|
+
return `${error.message}
|
|
504
|
+
\u2192 ${migration2.notice} See ${migration2.docs_url}`;
|
|
505
|
+
}
|
|
506
|
+
return `${error.message}
|
|
507
|
+
\u2192 Portal unreachable. Retry later, or call \`ckan_status_show\` to check portal health (its answer can come from the cache for up to an hour).`;
|
|
508
|
+
}
|
|
480
509
|
if (!(error instanceof CkanApiError)) {
|
|
481
510
|
return error instanceof Error ? error.message : String(error);
|
|
482
511
|
}
|
|
@@ -496,6 +525,8 @@ function formatCkanError(error, _toolName) {
|
|
|
496
525
|
hint = "\u2192 Use `ckan_package_search` to find a valid dataset name or ID.";
|
|
497
526
|
} else if (action === "organization_show") {
|
|
498
527
|
hint = "\u2192 Use `ckan_organization_list` or `ckan_organization_search` to discover valid organization names.";
|
|
528
|
+
} else if (action === "group_show") {
|
|
529
|
+
hint = "\u2192 Use `ckan_group_list` or `ckan_group_search` to discover valid group names.";
|
|
499
530
|
}
|
|
500
531
|
} else if (status === 400) {
|
|
501
532
|
if (action === "datastore_search_sql") {
|
|
@@ -506,9 +537,9 @@ function formatCkanError(error, _toolName) {
|
|
|
506
537
|
} else if (status === 409 || status === 422) {
|
|
507
538
|
hint = "\u2192 Portal rejected the request \u2014 parameters may conflict; simplify filters and retry.";
|
|
508
539
|
} else if (status === 503 || status === 502 || status === 504) {
|
|
509
|
-
hint = "\u2192 Portal temporarily unavailable \u2014 retry in a few seconds.";
|
|
540
|
+
hint = "\u2192 Portal temporarily unavailable \u2014 retry in a few seconds, or call `ckan_status_show` to check portal health (cached up to an hour).";
|
|
510
541
|
} else if (status === 500) {
|
|
511
|
-
hint = "\u2192 Portal internal error \u2014 try a different portal or retry later.";
|
|
542
|
+
hint = "\u2192 Portal internal error \u2014 try a different portal or retry later, and call `ckan_status_show` to check portal health (cached up to an hour).";
|
|
512
543
|
} else if (status === void 0) {
|
|
513
544
|
hint = "\u2192 The portal may not support this action, or the endpoint is unavailable.";
|
|
514
545
|
}
|
|
@@ -721,10 +752,10 @@ function validateServerUrl(serverUrl) {
|
|
|
721
752
|
try {
|
|
722
753
|
parsed = new URL(serverUrl);
|
|
723
754
|
} catch {
|
|
724
|
-
throw new
|
|
755
|
+
throw new SsrfGuardError(`Invalid URL: ${serverUrl}`);
|
|
725
756
|
}
|
|
726
757
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
727
|
-
throw new
|
|
758
|
+
throw new SsrfGuardError(`Disallowed protocol "${parsed.protocol}". Only http and https are allowed.`);
|
|
728
759
|
}
|
|
729
760
|
const hostname = parsed.hostname.toLowerCase();
|
|
730
761
|
const BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -733,18 +764,18 @@ function validateServerUrl(serverUrl) {
|
|
|
733
764
|
"ip6-loopback"
|
|
734
765
|
]);
|
|
735
766
|
if (BLOCKED_HOSTNAMES.has(hostname)) {
|
|
736
|
-
throw new
|
|
767
|
+
throw new SsrfGuardError(`Access to "${hostname}" is not allowed.`);
|
|
737
768
|
}
|
|
738
769
|
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) && isBlockedIp(hostname)) {
|
|
739
|
-
throw new
|
|
770
|
+
throw new SsrfGuardError(`Access to private/internal IP addresses is not allowed.`);
|
|
740
771
|
}
|
|
741
772
|
if (hostname.startsWith("[") && isBlockedIp(hostname.slice(1, -1))) {
|
|
742
|
-
throw new
|
|
773
|
+
throw new SsrfGuardError(`Access to private/internal IPv6 addresses is not allowed.`);
|
|
743
774
|
}
|
|
744
775
|
const rawAllowed = typeof process !== "undefined" ? process.env.CKAN_ALLOWED_DOMAINS ?? "" : "";
|
|
745
776
|
const allowedDomains = rawAllowed.split(",").map((s) => s.trim()).filter(Boolean);
|
|
746
777
|
if (allowedDomains.length > 0 && !allowedDomains.includes(hostname)) {
|
|
747
|
-
throw new
|
|
778
|
+
throw new SsrfGuardError(`Domain "${hostname}" is not in the allowed list (CKAN_ALLOWED_DOMAINS).`);
|
|
748
779
|
}
|
|
749
780
|
}
|
|
750
781
|
function assertHttpAllowlistConfigured() {
|
|
@@ -776,7 +807,7 @@ function createSsrfSafeLookup(dnsModule) {
|
|
|
776
807
|
const list = Array.isArray(addresses) ? addresses : [addresses];
|
|
777
808
|
for (const a of list) {
|
|
778
809
|
if (isBlockedIp(a.address)) {
|
|
779
|
-
callback(new
|
|
810
|
+
callback(new SsrfGuardError(
|
|
780
811
|
`Access to private/internal IP addresses is not allowed ("${hostname}" resolves to ${a.address}).`
|
|
781
812
|
));
|
|
782
813
|
return;
|
|
@@ -844,11 +875,11 @@ async function assertHostnameResolvesSafe(hostname) {
|
|
|
844
875
|
try {
|
|
845
876
|
addresses = await lookup(hostname);
|
|
846
877
|
} catch {
|
|
847
|
-
throw new
|
|
878
|
+
throw new SsrfGuardError(`Cannot resolve "${hostname}" for SSRF validation (failing closed).`);
|
|
848
879
|
}
|
|
849
880
|
for (const a of addresses) {
|
|
850
881
|
if (isBlockedIp(a.address)) {
|
|
851
|
-
throw new
|
|
882
|
+
throw new SsrfGuardError(
|
|
852
883
|
`Access to private/internal IP addresses is not allowed ("${hostname}" resolves to ${a.address}).`
|
|
853
884
|
);
|
|
854
885
|
}
|
|
@@ -994,6 +1025,14 @@ async function makeCkanRequest(serverUrl, action, params = {}, opts = {}) {
|
|
|
994
1025
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
995
1026
|
}
|
|
996
1027
|
}, { maxHops: 5 });
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
if (controller.signal.aborted) {
|
|
1030
|
+
throw new CkanTransportError(`Request timeout connecting to ${serverUrl}`, serverUrl);
|
|
1031
|
+
}
|
|
1032
|
+
if (error instanceof TypeError) {
|
|
1033
|
+
throw new CkanTransportError(`Network error: ${error.message}`, serverUrl);
|
|
1034
|
+
}
|
|
1035
|
+
throw error;
|
|
997
1036
|
} finally {
|
|
998
1037
|
clearTimeout(timeoutId);
|
|
999
1038
|
}
|
|
@@ -1048,17 +1087,19 @@ async function makeCkanRequest(serverUrl, action, params = {}, opts = {}) {
|
|
|
1048
1087
|
if (error instanceof CkanApiError) throw error;
|
|
1049
1088
|
if (axios.isAxiosError(error)) {
|
|
1050
1089
|
const axiosError = error;
|
|
1090
|
+
const guardError = unwrapSsrfGuardError(axiosError);
|
|
1091
|
+
if (guardError) throw guardError;
|
|
1051
1092
|
if (axiosError.response) {
|
|
1052
1093
|
const status = axiosError.response.status;
|
|
1053
1094
|
const data = axiosError.response.data;
|
|
1054
1095
|
const errorMsg = data?.error?.message || data?.error || "Unknown error";
|
|
1055
1096
|
throw new CkanApiError(`CKAN API error (${status}): ${errorMsg}`, status, action, serverUrl);
|
|
1056
1097
|
} else if (axiosError.code === "ECONNABORTED") {
|
|
1057
|
-
throw new
|
|
1098
|
+
throw new CkanTransportError(`Request timeout connecting to ${serverUrl}`, serverUrl);
|
|
1058
1099
|
} else if (axiosError.code === "ENOTFOUND") {
|
|
1059
|
-
throw new
|
|
1100
|
+
throw new CkanTransportError(`Server not found: ${serverUrl}`, serverUrl);
|
|
1060
1101
|
} else {
|
|
1061
|
-
throw new
|
|
1102
|
+
throw new CkanTransportError(`Network error: ${axiosError.message}`, serverUrl);
|
|
1062
1103
|
}
|
|
1063
1104
|
}
|
|
1064
1105
|
throw error;
|
|
@@ -6048,7 +6089,7 @@ var registerAllPrompts = (server) => {
|
|
|
6048
6089
|
function createServer() {
|
|
6049
6090
|
return new McpServer({
|
|
6050
6091
|
name: "ckan-mcp-server",
|
|
6051
|
-
version: "0.4.
|
|
6092
|
+
version: "0.4.129"
|
|
6052
6093
|
});
|
|
6053
6094
|
}
|
|
6054
6095
|
function registerAll(server) {
|