@aborruso/ckan-mcp-server 0.4.113 → 0.4.115

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 CHANGED
@@ -1,7 +1,54 @@
1
1
  # LOG
2
2
 
3
+ ## 2026-08-03
4
+
5
+ ### v0.4.115
6
+
7
+ First release published from CI. No functional change: this exists to exercise the release workflow added earlier today end to end — tag guard, OIDC authentication, provenance attestation — rather than discovering whether it works during a release that actually matters.
8
+
9
+
10
+
11
+ ### npm provenance: release workflow on tags
12
+
13
+ `npm view @aborruso/ckan-mcp-server@0.4.114 dist.attestations` came back empty — packages were published by hand from a local machine, with nothing binding a tarball to the commit that produced it. Adopters could check *which* version was current (see below) but not *where it came from*.
14
+
15
+ - New `.github/workflows/release.yml`: triggers on `v*` tags, verifies the tag matches `package.json` **before** anything else (npm publishes are irreversible after 72h), then `npm ci` → build → tests → `npm publish --provenance`. Guard tested both ways; `npm test -- --run` confirmed against the current 461-test suite.
16
+ - `package.json` had **no `repository` field** — a hard prerequisite for provenance, which would have failed the publish. Set to `ondata/ckan-mcp-server`.
17
+ - Release workflow in `CLAUDE.md` rewritten rather than extended: step 5 now warns that pushing the tag *is* the publish, step 9 says explicitly not to run `npm publish` by hand (two paths would collide on `EPUBLISHCONFLICT`), and step 10 must wait for the CI run to go green because the MCP Registry validates that the npm version exists.
18
+ - `.readme-full.md` added to `.gitignore`: the `prepack`/`postpack` pair swaps in the short npm README, so a local publish failing between the two hooks leaves the wrong `README.md` in the tree, one `git add .` away from being committed.
19
+ - Authentication is **trusted publishing (OIDC)**, not a secret. The first draft used an `NPM_TOKEN`; `npm profile get` then surfaced npm's own warning that "tokens that bypass 2FA are being restricted for direct publishing", which pointed at the mechanism npm now recommends instead. Trusted publishing needs no stored credential, emits provenance by default, and — because the trusted publisher names the GitHub repo explicitly — also settles the open question about the npm scope (`@aborruso`) differing from the GitHub org (`ondata`). The workflow upgrades npm to ≥ 11.5.1 explicitly: Node 22 ships npm 10.x, which falls back to token auth *silently* and would publish unattested. `--provenance` is kept although implied, so that degradation fails the job instead of passing quietly.
20
+ - **Not yet active**: the trusted publisher has to be registered by hand on npmjs.com (package → Settings → Trusted Publisher → GitHub Actions → `ondata` / `ckan-mcp-server` / `release.yml`). Until then the workflow runs and fails at the publish step. Note that the workflow filename is part of that identity.
21
+
22
+ ### MCP Registry entry realigned to 0.4.114
23
+
24
+ Published and verified against the public endpoint: the registry now serves **0.4.114** with `isLatest: true` (updated 2026-08-03T06:01:07Z), and the old 0.4.83 record dropped to `isLatest: false`.
25
+
26
+ The official MCP Registry entry for `io.github.aborruso/ckan-mcp-server` had been stuck at **0.4.83 since 2026-03-12** — 31 patch releases and almost five months behind npm, while still flagged `isLatest: true`. Clients resolving the server through the registry were pointed at a build predating the v0.4.108 SSRF remediation, and 0.4.83 is still installable from npm.
27
+
28
+ - Root cause, not a one-off slip: the Release Workflow in `CLAUDE.md` listed the version bump for `package.json` and `manifest.json` but never `server.json`, and `npm publish` does not touch the registry. The drift was structural and would have kept growing.
29
+ - Fixed `server.json` (both `version` and `packages[0].version` — two fields, easy to half-update) and rewrote the release workflow: `server.json` added to step 1, a new step 10 for `mcp-publisher publish` placed after `npm publish` since the registry validates that the npm version exists, plus a `curl` one-liner to verify the published entry.
30
+ - Surfaced by an unsolicited vendor email selling a £395 "MCP Readiness Audit". The sales pitch was worthless — the remedy it offered has nothing to do with the defect — but the three technical claims all checked out under verification. Worth recording: the finding was real and cost the sender two `curl` calls, which is exactly how long it would have taken us to catch it ourselves with a check in the release procedure.
31
+ - Registry tokens (`.mcpregistry_*`) verified: gitignored, never committed.
32
+
3
33
  ## 2026-07-31
4
34
 
35
+ ### v0.4.114
36
+
37
+ structuredContent capped like the text (closes #39).
38
+
39
+ The v0.4.113 cap applied to `content[].text` only, so any client reading the structured channel got the full payload — `ckan_tag_list limit=1000` on dati.gov.it: ~50K of text against 65,382 uncapped characters. The limit was a fiction for those clients.
40
+
41
+ - New `jsonToolResult()`: truncates once via `truncateJson` and parses the result back into `structuredContent`, so the two channels cannot disagree and `_truncated`/`_original_count` reach structured readers too. Applied to 13 call sites.
42
+ - `ckan_status_show` and `ckan_find_portals` pair structured output with Markdown text, so there is no truncated JSON to derive it from: they cap it on its own via the new `cappedStructured()`. My first pass left them uncapped calling them "bounded by shape" — wrong, as review pointed out: `status_show` is echoed straight from the portal, and the 50-result limit on `find_portals` bounds the number of entries, not the length of their titles and URLs. Only the `all_fields=false` count branches of `ckan_organization_list`/`ckan_group_list` stay outside the cap: a single integer.
43
+ - **Correction**: the deferral in v0.4.113 claimed capping would drop rows from `datastore-table-ui`. That was wrong and never verified — the UI resource is commented out in `src/resources/index.ts:18` and never registered, and `ckan_datastore_search` returns no `structuredContent` at all. No exception was needed. Same failure mode as the bug being fixed: a plausible claim about the code that nobody checked.
44
+ - 7 new tests (461 total), covering a payload long in a single field rather than in many, and a bounded-count list whose entries are individually oversized. Docs realigned: `README.md`, `docs/JSON-OUTPUT.md`, `docs/DECISIONS.md`, `CLAUDE.md` all asserted the uncapped behaviour.
45
+ - Released end to end: tag, GitHub release with DXT and skill, npm `0.4.114`, Cloudflare version `36b611fd`. Verified on the public endpoint after waiting for edge propagation — `tag_list limit=1000`: 49,938 characters on both channels, byte-identical, `_truncated` set. `status_show` and `find_portals` legitimately differ across channels: Markdown text against JSON structured output, two renderings of the same data.
46
+ - Process note: check edge propagation with an active probe, not a fixed wait. The v0.4.113 verification measured the old code because it ran seconds after deploy, and briefly looked like a broken release.
47
+
48
+ ### v0.4.113 — release addendum
49
+
50
+ Released the same day: tag, GitHub release with DXT and skill, npm `0.4.113`, Cloudflare version `1cac40ea`. Also merged #38 (`docs: document user-facing limits`, contributed by `averyquinnhq`), which closed #37, and disabled the `claude-code-review` workflow: it cannot pass on fork PRs, since GitHub withholds secrets and the OIDC token for `pull_request` events from forks, so the job dies on token setup regardless of the diff. The file is kept in the repo. `CONTRIBUTING.md` now also states that `build:tsc` is not a gate — a contributor exhausted 8 GB of heap on it before reporting they could not pass it, because the warning lived only in `CLAUDE.md`.
51
+
5
52
  ### v0.4.113
6
53
 
7
54
  JSON output always parseable (issue #39).
package/dist/index.js CHANGED
@@ -1049,6 +1049,16 @@ function truncateJson(obj, limit = CHARACTER_LIMIT) {
1049
1049
  const bare = JSON.stringify({ _truncated: true });
1050
1050
  return bare.length <= limit ? bare : "{}";
1051
1051
  }
1052
+ function jsonToolResult(obj, limit = CHARACTER_LIMIT) {
1053
+ const text = truncateJson(obj, limit);
1054
+ return {
1055
+ content: [{ type: "text", text }],
1056
+ structuredContent: JSON.parse(text)
1057
+ };
1058
+ }
1059
+ function cappedStructured(obj, limit = CHARACTER_LIMIT) {
1060
+ return JSON.parse(truncateJson(obj, limit));
1061
+ }
1052
1062
  function formatError(message, asJson) {
1053
1063
  return asJson ? JSON.stringify({ error: message, _error: true }, null, 2) : message;
1054
1064
  }
@@ -2158,10 +2168,7 @@ Typical workflow: ckan_find_relevant_datasets \u2192 ckan_package_show (inspect
2158
2168
  results: top
2159
2169
  };
2160
2170
  if (params.response_format === "json" /* JSON */) {
2161
- return {
2162
- content: [{ type: "text", text: truncateJson(payload) }],
2163
- structuredContent: payload
2164
- };
2171
+ return jsonToolResult(payload);
2165
2172
  }
2166
2173
  let markdown = `# Relevant CKAN Datasets
2167
2174
 
@@ -2305,10 +2312,7 @@ Typical workflow: ckan_package_show \u2192 pick a resource with datastore_active
2305
2312
  );
2306
2313
  if (params.response_format === "json" /* JSON */) {
2307
2314
  const compact = compactPackageShow(enrichPackageShowResult(result), params.server_url);
2308
- return {
2309
- content: [{ type: "text", text: truncateJson(compact) }],
2310
- structuredContent: compact
2311
- };
2315
+ return jsonToolResult(compact);
2312
2316
  }
2313
2317
  const markdown = formatPackageShowMarkdown(result, params.server_url);
2314
2318
  return {
@@ -2417,10 +2421,7 @@ dataset's own resource URLs); set check_source_portal=true to enable it.`,
2417
2421
  format_filter: formatFilter ?? null,
2418
2422
  resources: summary
2419
2423
  };
2420
- return {
2421
- content: [{ type: "text", text: truncateJson(payload) }],
2422
- structuredContent: payload
2423
- };
2424
+ return jsonToolResult(payload);
2424
2425
  }
2425
2426
  let markdown = `# Resources: ${result.title || result.name}
2426
2427
 
@@ -2690,10 +2691,7 @@ Typical workflow: ckan_organization_list \u2192 ckan_organization_show (inspect
2690
2691
  }));
2691
2692
  if (params.response_format === "json" /* JSON */) {
2692
2693
  const output = { count: items.length, organizations };
2693
- return {
2694
- content: [{ type: "text", text: truncateJson(output) }],
2695
- structuredContent: output
2696
- };
2694
+ return jsonToolResult(output);
2697
2695
  }
2698
2696
  let markdown2 = `# CKAN Organizations
2699
2697
 
@@ -2726,10 +2724,7 @@ Note: organization_list returned 500; using package_search facets.
2726
2724
  }
2727
2725
  if (params.response_format === "json" /* JSON */) {
2728
2726
  const compact = compactOrganizationList(result);
2729
- return {
2730
- content: [{ type: "text", text: truncateJson(compact) }],
2731
- structuredContent: compact
2732
- };
2727
+ return jsonToolResult(compact);
2733
2728
  }
2734
2729
  let markdown = `# CKAN Organizations
2735
2730
 
@@ -2818,10 +2813,7 @@ Typical workflow: ckan_organization_show \u2192 ckan_package_show (inspect a dat
2818
2813
  );
2819
2814
  if (params.response_format === "json" /* JSON */) {
2820
2815
  const compact = compactOrganizationShow(result, params.server_url);
2821
- return {
2822
- content: [{ type: "text", text: truncateJson(compact) }],
2823
- structuredContent: compact
2824
- };
2816
+ return jsonToolResult(compact);
2825
2817
  }
2826
2818
  const markdown = formatOrganizationShowMarkdown(result, params.server_url);
2827
2819
  return {
@@ -2895,10 +2887,7 @@ Typical workflow: ckan_organization_search \u2192 ckan_organization_show (get de
2895
2887
  view_url: getOrganizationViewUrl(params.server_url, { name: item.name })
2896
2888
  }))
2897
2889
  };
2898
- return {
2899
- content: [{ type: "text", text: truncateJson(jsonResult) }],
2900
- structuredContent: jsonResult
2901
- };
2890
+ return jsonToolResult(jsonResult);
2902
2891
  }
2903
2892
  let markdown = `# CKAN Organization Search Results
2904
2893
 
@@ -3199,10 +3188,7 @@ Security note: SQL queries are forwarded directly to the CKAN DataStore API. The
3199
3188
  );
3200
3189
  if (params.response_format === "json" /* JSON */) {
3201
3190
  const compact = compactDatastoreResult(result);
3202
- return {
3203
- content: [{ type: "text", text: truncateJson(compact) }],
3204
- structuredContent: compact
3205
- };
3191
+ return jsonToolResult(compact);
3206
3192
  }
3207
3193
  const markdown = formatDatastoreSqlMarkdown(result, params.server_url, params.sql);
3208
3194
  return {
@@ -3274,7 +3260,7 @@ Typical workflow: ckan_status_show (verify server is up) \u2192 ckan_package_sea
3274
3260
  const markdown = formatStatusMarkdown(result, params.server_url, hvdCount);
3275
3261
  return {
3276
3262
  content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }],
3277
- structuredContent: result
3263
+ structuredContent: cappedStructured(result)
3278
3264
  };
3279
3265
  } catch (error) {
3280
3266
  return {
@@ -3384,10 +3370,7 @@ Typical workflow: ckan_tag_list \u2192 ckan_package_search with fq="tags:tag_nam
3384
3370
  count: tags.length,
3385
3371
  tags
3386
3372
  };
3387
- return {
3388
- content: [{ type: "text", text: truncateJson(output) }],
3389
- structuredContent: output
3390
- };
3373
+ return jsonToolResult(output);
3391
3374
  }
3392
3375
  let markdown = `# CKAN Tags
3393
3376
 
@@ -3620,10 +3603,7 @@ Typical workflow: ckan_group_list \u2192 ckan_group_show (inspect one) \u2192 ck
3620
3603
  );
3621
3604
  if (params.response_format === "json" /* JSON */) {
3622
3605
  const compact = compactGroupList(result);
3623
- return {
3624
- content: [{ type: "text", text: truncateJson(compact) }],
3625
- structuredContent: compact
3626
- };
3606
+ return jsonToolResult(compact);
3627
3607
  }
3628
3608
  let markdown = `# CKAN Groups
3629
3609
 
@@ -3709,10 +3689,7 @@ Typical workflow: ckan_group_show \u2192 ckan_package_show (inspect a dataset) \
3709
3689
  );
3710
3690
  if (params.response_format === "json" /* JSON */) {
3711
3691
  const compact = compactGroupShow(result);
3712
- return {
3713
- content: [{ type: "text", text: truncateJson(compact) }],
3714
- structuredContent: compact
3715
- };
3692
+ return jsonToolResult(compact);
3716
3693
  }
3717
3694
  const markdown = formatGroupShowMarkdown(result, params.server_url);
3718
3695
  return {
@@ -3781,10 +3758,7 @@ Typical workflow: ckan_group_search \u2192 ckan_group_show (get details) \u2192
3781
3758
  dataset_count: group.count
3782
3759
  }))
3783
3760
  };
3784
- return {
3785
- content: [{ type: "text", text: truncateJson(jsonResult) }],
3786
- structuredContent: jsonResult
3787
- };
3761
+ return jsonToolResult(jsonResult);
3788
3762
  }
3789
3763
  let markdown = `# CKAN Group Search Results
3790
3764
 
@@ -4986,10 +4960,7 @@ Typical workflow: sparql_query (explore schema) \u2192 sparql_query (targeted qu
4986
4960
  const data = await querySparqlEndpoint(params.endpoint_url, limitedQuery);
4987
4961
  if (params.response_format === "json" /* JSON */) {
4988
4962
  const result = formatSparqlJson(data);
4989
- return {
4990
- content: [{ type: "text", text: truncateJson(result) }],
4991
- structuredContent: result
4992
- };
4963
+ return jsonToolResult(result);
4993
4964
  }
4994
4965
  return {
4995
4966
  content: [{ type: "text", text: truncateText(formatSparqlMarkdown(data, params.endpoint_url)) }]
@@ -5111,7 +5082,7 @@ Typical workflow: ckan_find_portals (discover portal URL) \u2192 ckan_status_sho
5111
5082
  const markdown = formatMarkdown(results, active.length, params.limit);
5112
5083
  return {
5113
5084
  content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }],
5114
- structuredContent: { portals: results.map((p) => ({
5085
+ structuredContent: cappedStructured({ portals: results.map((p) => ({
5115
5086
  url: p.Href,
5116
5087
  title: p.SiteInfo.site_title,
5117
5088
  country: p.Coordinates.country_name,
@@ -5119,7 +5090,7 @@ Typical workflow: ckan_find_portals (discover portal URL) \u2192 ckan_status_sho
5119
5090
  datasets: p.DatasetsNumber,
5120
5091
  locale: p.SiteInfo.locale_default,
5121
5092
  has_datastore: (p.Plugins || []).includes("datastore")
5122
- })) }
5093
+ })) })
5123
5094
  };
5124
5095
  } catch (error) {
5125
5096
  return {
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@aborruso/ckan-mcp-server",
3
- "version": "0.4.113",
3
+ "version": "0.4.115",
4
4
  "mcpName": "io.github.aborruso/ckan-mcp-server",
5
5
  "description": "MCP server for interacting with CKAN open data portals",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ondata/ckan-mcp-server.git"
9
+ },
6
10
  "main": "dist/index.js",
7
11
  "type": "module",
8
12
  "bin": {