@aborruso/ckan-mcp-server 0.4.130 → 0.4.131
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 +14 -0
- package/dist/index.js +99 -83
- package/package.json +1 -1
package/LOG.md
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
# LOG
|
|
2
2
|
|
|
3
|
+
## 2026-10-22
|
|
4
|
+
|
|
5
|
+
### v0.4.131
|
|
6
|
+
|
|
7
|
+
Ships #563: Anubis challenge retry with an honest User-Agent (#7). Tests 595 passed, smoke 17/17.
|
|
8
|
+
|
|
9
|
+
### Anubis-fronted portals: retry once with an honest User-Agent (#7)
|
|
10
|
+
|
|
11
|
+
Reported by @umlwbin: our Chrome-impersonating headers (needed by dati.anticorruzione.it's WAF) trip Anubis anti-bot gates, which answer with an HTML proof-of-work page that axios cannot solve. Anubis challenges any UA containing `Mozilla`, including `Mozilla/5.0 (compatible; ...)`, so no fixed UA serves both portals. `makeCkanRequest` now detects the challenge page (`anubis_challenge` marker, `isAnubisChallenge`) and retries once with `CKAN-MCP-Server/1.0` and no `Sec-CH-UA*` headers, on both the axios and fetch/Workers paths. The retry acquires its own rate-limit token (Greptile review). +6 tests (fail without the fix). Tests 595 passed; e2e `package_search` OK on canwin-datahub.ad.umanitoba.ca/data (48,947), dati.anticorruzione.it (70), dati.gov.it.
|
|
12
|
+
|
|
3
13
|
## 2026-10-19
|
|
4
14
|
|
|
15
|
+
### Advisory GHSA-w3gp-3hrh-v465 published
|
|
16
|
+
|
|
17
|
+
Reported by @olevasseur, accepted and published as Medium (CVSS 6.5): affected `>= 0.4.114, <= 0.4.129`, patched `0.4.130`. CVE requested, not yet assigned. v0.4.130 is live on npm (with provenance), the MCP Registry and the Worker. `SECURITY.md` table updated.
|
|
18
|
+
|
|
5
19
|
### v0.4.130
|
|
6
20
|
|
|
7
21
|
Ships #561: linear shrink in `truncateJson` (GHSA-w3gp-3hrh-v465). Tests 589 passed, smoke 17/17.
|
package/dist/index.js
CHANGED
|
@@ -932,6 +932,11 @@ function auditLog(serverUrl, action, params, cacheHit) {
|
|
|
932
932
|
} catch {
|
|
933
933
|
}
|
|
934
934
|
}
|
|
935
|
+
var BROWSER_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
936
|
+
var HONEST_USER_AGENT = "CKAN-MCP-Server/1.0";
|
|
937
|
+
function isAnubisChallenge(body) {
|
|
938
|
+
return typeof body === "string" && body.includes("anubis_challenge");
|
|
939
|
+
}
|
|
935
940
|
async function makeCkanRequest(serverUrl, action, params = {}, opts = {}) {
|
|
936
941
|
const isNode = typeof process !== "undefined" && !!process.versions?.node;
|
|
937
942
|
validateServerUrl(serverUrl);
|
|
@@ -963,95 +968,106 @@ async function makeCkanRequest(serverUrl, action, params = {}, opts = {}) {
|
|
|
963
968
|
_lastCacheHit = false;
|
|
964
969
|
const rateLimitConfig = getRateLimitConfig();
|
|
965
970
|
const rateLimitEnabled = rateLimitConfig.enabled && opts.rateLimit !== false;
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
971
|
+
const acquireRateToken = async () => {
|
|
972
|
+
if (rateLimitEnabled) {
|
|
973
|
+
const hostname = new URL(resolvedServerUrl).hostname;
|
|
974
|
+
await getRateLimiter().acquire(hostname);
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
await acquireRateToken();
|
|
970
978
|
try {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
headers: {
|
|
992
|
-
Accept: "application/json, text/plain, */*",
|
|
993
|
-
"Accept-Language": "en-US,en;q=0.9,it;q=0.8",
|
|
994
|
-
"Accept-Encoding": "gzip, deflate, br",
|
|
995
|
-
Connection: "keep-alive",
|
|
996
|
-
"Sec-CH-UA": '"Chromium";v="120", "Not?A_Brand";v="24", "Google Chrome";v="120"',
|
|
997
|
-
"Sec-CH-UA-Mobile": "?0",
|
|
998
|
-
"Sec-CH-UA-Platform": '"Linux"',
|
|
999
|
-
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
decodedData = await decodePossiblyCompressed(
|
|
1003
|
-
response.data,
|
|
1004
|
-
response.headers
|
|
1005
|
-
);
|
|
1006
|
-
} else {
|
|
1007
|
-
const searchParams = new URLSearchParams();
|
|
1008
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1009
|
-
if (value === void 0 || value === null) {
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
searchParams.set(key, String(value));
|
|
1013
|
-
}
|
|
1014
|
-
const fetchUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url;
|
|
1015
|
-
const controller = new AbortController();
|
|
1016
|
-
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1017
|
-
let response;
|
|
1018
|
-
try {
|
|
1019
|
-
response = await safeFetch(fetchUrl, {
|
|
1020
|
-
method: "GET",
|
|
1021
|
-
signal: controller.signal,
|
|
979
|
+
const fetchDecoded = async (honest) => {
|
|
980
|
+
if (isNode) {
|
|
981
|
+
const safeAgents = await getSafeAgents();
|
|
982
|
+
const response = await axios.get(url, {
|
|
983
|
+
params,
|
|
984
|
+
timeout: 3e4,
|
|
985
|
+
responseType: "arraybuffer",
|
|
986
|
+
maxRedirects: 5,
|
|
987
|
+
// Re-validate every redirect hop: Node skips the agent's `lookup` for IP
|
|
988
|
+
// literals, so a public host 302-ing to e.g. 169.254.169.254 would otherwise
|
|
989
|
+
// be followed. Also enforces CKAN_ALLOWED_DOMAINS on redirect targets.
|
|
990
|
+
beforeRedirect: (redirectOptions) => {
|
|
991
|
+
validateServerUrl(redirectOptions.href);
|
|
992
|
+
},
|
|
993
|
+
// Never route through HTTP_PROXY/HTTPS_PROXY: a proxy would connect to the
|
|
994
|
+
// target itself, bypassing the SSRF-safe lookup pinned in the agents below.
|
|
995
|
+
proxy: false,
|
|
996
|
+
maxContentLength: MAX_RESPONSE_BYTES,
|
|
997
|
+
maxBodyLength: MAX_RESPONSE_BYTES,
|
|
998
|
+
...safeAgents ? { httpAgent: safeAgents.httpAgent, httpsAgent: safeAgents.httpsAgent } : {},
|
|
1022
999
|
headers: {
|
|
1023
1000
|
Accept: "application/json, text/plain, */*",
|
|
1024
|
-
"Accept-
|
|
1025
|
-
"
|
|
1001
|
+
"Accept-Language": "en-US,en;q=0.9,it;q=0.8",
|
|
1002
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
1003
|
+
Connection: "keep-alive",
|
|
1004
|
+
...honest ? {} : {
|
|
1005
|
+
"Sec-CH-UA": '"Chromium";v="120", "Not?A_Brand";v="24", "Google Chrome";v="120"',
|
|
1006
|
+
"Sec-CH-UA-Mobile": "?0",
|
|
1007
|
+
"Sec-CH-UA-Platform": '"Linux"'
|
|
1008
|
+
},
|
|
1009
|
+
"User-Agent": honest ? HONEST_USER_AGENT : BROWSER_USER_AGENT
|
|
1026
1010
|
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1011
|
+
});
|
|
1012
|
+
return decodePossiblyCompressed(
|
|
1013
|
+
response.data,
|
|
1014
|
+
response.headers
|
|
1015
|
+
);
|
|
1016
|
+
} else {
|
|
1017
|
+
const searchParams = new URLSearchParams();
|
|
1018
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1019
|
+
if (value === void 0 || value === null) {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
searchParams.set(key, String(value));
|
|
1031
1023
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1024
|
+
const fetchUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url;
|
|
1025
|
+
const controller = new AbortController();
|
|
1026
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1027
|
+
let response;
|
|
1028
|
+
try {
|
|
1029
|
+
response = await safeFetch(fetchUrl, {
|
|
1030
|
+
method: "GET",
|
|
1031
|
+
signal: controller.signal,
|
|
1032
|
+
headers: {
|
|
1033
|
+
Accept: "application/json, text/plain, */*",
|
|
1034
|
+
"Accept-Encoding": "identity",
|
|
1035
|
+
"User-Agent": honest ? HONEST_USER_AGENT : BROWSER_USER_AGENT
|
|
1036
|
+
}
|
|
1037
|
+
}, { maxHops: 5 });
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
if (controller.signal.aborted) {
|
|
1040
|
+
throw new CkanTransportError(`Request timeout connecting to ${serverUrl}`, serverUrl);
|
|
1041
|
+
}
|
|
1042
|
+
if (error instanceof TypeError) {
|
|
1043
|
+
throw new CkanTransportError(`Network error: ${error.message}`, serverUrl);
|
|
1044
|
+
}
|
|
1045
|
+
throw error;
|
|
1046
|
+
} finally {
|
|
1047
|
+
clearTimeout(timeoutId);
|
|
1034
1048
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
+
if (!response.ok) {
|
|
1050
|
+
throw new CkanApiError(`CKAN API error (${response.status}): ${response.statusText}`, response.status, action, serverUrl);
|
|
1051
|
+
}
|
|
1052
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
1053
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_BYTES) {
|
|
1054
|
+
throw new CkanApiError(`Response too large (${declaredLength} bytes)`, void 0, action, serverUrl);
|
|
1055
|
+
}
|
|
1056
|
+
const buffer = await response.arrayBuffer();
|
|
1057
|
+
if (buffer.byteLength > MAX_RESPONSE_BYTES) {
|
|
1058
|
+
throw new CkanApiError(`Response too large (${buffer.byteLength} bytes)`, void 0, action, serverUrl);
|
|
1059
|
+
}
|
|
1060
|
+
const headers = {};
|
|
1061
|
+
response.headers.forEach((headerValue, headerKey) => {
|
|
1062
|
+
headers[headerKey] = headerValue;
|
|
1063
|
+
});
|
|
1064
|
+
return decodePossiblyCompressed(buffer, headers);
|
|
1049
1065
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
decodedData = await
|
|
1066
|
+
};
|
|
1067
|
+
let decodedData = await fetchDecoded(false);
|
|
1068
|
+
if (isAnubisChallenge(decodedData)) {
|
|
1069
|
+
await acquireRateToken();
|
|
1070
|
+
decodedData = await fetchDecoded(true);
|
|
1055
1071
|
}
|
|
1056
1072
|
if (decodedData && decodedData.success === true) {
|
|
1057
1073
|
const result = decodedData.result;
|
|
@@ -6101,7 +6117,7 @@ var registerAllPrompts = (server) => {
|
|
|
6101
6117
|
function createServer() {
|
|
6102
6118
|
return new McpServer({
|
|
6103
6119
|
name: "ckan-mcp-server",
|
|
6104
|
-
version: "0.4.
|
|
6120
|
+
version: "0.4.131"
|
|
6105
6121
|
});
|
|
6106
6122
|
}
|
|
6107
6123
|
function registerAll(server) {
|