@aborruso/ckan-mcp-server 0.4.115 → 0.4.116

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.
Files changed (3) hide show
  1. package/LOG.md +24 -0
  2. package/dist/index.js +30 -13
  3. package/package.json +1 -1
package/LOG.md CHANGED
@@ -1,11 +1,35 @@
1
1
  # LOG
2
2
 
3
+ ## 2026-08-04
4
+
5
+ ### v0.4.116
6
+
7
+ Ships the two DataStore output fixes below, plus `.greptile/rules.md`: the automated review on #43 produced one confident false positive, so the invariants a generic reviewer cannot know are now stated in the repo.
8
+
9
+ ### DataStore tables no longer hide columns silently
10
+
11
+ Both markdown renderers in `src/tools/datastore.ts` cut the record table at 8 columns with no notice. On the Messina electoral-lists resource (14 columns) the table stopped right before `cognome`, `nome`, `sesso` and `voti`: a model reading it saw an election dataset with no votes and no candidate names, and nothing told it anything was missing. Row truncation was already handled properly (`... and N more records`, `Total Records`); columns were not.
12
+
13
+ Both renderers now append a note naming the omitted columns and pointing at the way to retrieve them (`fields` parameter for search, an explicit SELECT list for SQL, or `response_format: "json"`). The JSON path never had the bug — `compactDatastoreResult` passes every column through.
14
+
15
+ Found while checking what the GovInsider piece on the OKFN Brazil/Uruguay pilot added to `docs/future-ideas.md` (nothing new — it covers the same pilot already recorded on 2026-06-11), but its failure mode is exactly this: the model fills a gap it cannot see.
16
+
17
+ ### `_full_text` no longer eats a table slot
18
+
19
+ Surfaced by the end-to-end check above: `datastore_search_sql` on `SELECT *` returns CKAN's internal `_full_text` column, which repeats the whole row as one concatenated string. It was taking the first table slot and pushing out a real column, and the same query reported 15 columns via SQL against 14 via `datastore_search`. `_id` was already filtered; `_full_text` now is too, in both renderers and in the JSON output (where it was pure token waste). The two tools now agree on the column count.
20
+
21
+ 465 tests pass; verified end to end against `dati.comune.messina.it`.
22
+
3
23
  ## 2026-08-03
4
24
 
5
25
  ### v0.4.115
6
26
 
7
27
  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
28
 
29
+ Worked on the first try: the job went green in 34s and the attestation binds the tarball to `ondata/ckan-mcp-server`, workflow `release.yml`, ref `refs/tags/v0.4.115`, on a GitHub-hosted runner. `npm view @aborruso/ckan-mcp-server@0.4.115 dist.attestations` returns an SLSA v1 provenance; the same query on the hand-published 0.4.114 returns nothing.
30
+
31
+ One snag worth remembering: `mcp-publisher` failed with an expired JWT, and re-running `login github` then died with `incorrect_device_code` before the browser step. Neither error named the real cause — the local binary was **1.5.0 from 6 March**, and the device-auth flow changed by **1.8.0**. Updating the binary fixed it. When the registry token expires, check the publisher version too: both were installed the same day and go stale together.
32
+
9
33
 
10
34
 
11
35
  ### npm provenance: release workflow on tags
package/dist/index.js CHANGED
@@ -2936,6 +2936,20 @@ Typical workflow: ckan_organization_search \u2192 ckan_organization_show (get de
2936
2936
 
2937
2937
  // src/tools/datastore.ts
2938
2938
  import { z as z4 } from "zod";
2939
+ var MAX_TABLE_COLUMNS = 8;
2940
+ var INTERNAL_FIELDS = ["_id", "_full_text"];
2941
+ var isInternalField = (id) => INTERNAL_FIELDS.includes(id);
2942
+ var MAX_LISTED_OMITTED_COLUMNS = 15;
2943
+ var sanitizeFieldName = (id) => id.replace(/[\r\n]+/g, " ").replace(/\|/g, "\\|");
2944
+ function formatOmittedColumnsNote(allFields, hint) {
2945
+ if (allFields.length <= MAX_TABLE_COLUMNS) return "";
2946
+ const omitted = allFields.slice(MAX_TABLE_COLUMNS);
2947
+ const listed = omitted.slice(0, MAX_LISTED_OMITTED_COLUMNS).map(sanitizeFieldName).join(", ");
2948
+ const rest = omitted.length > MAX_LISTED_OMITTED_COLUMNS ? ` and ${omitted.length - MAX_LISTED_OMITTED_COLUMNS} more` : "";
2949
+ return `
2950
+ > **Note**: the table above shows only the first ${MAX_TABLE_COLUMNS} of ${allFields.length} columns. Columns not shown: ${listed}${rest}. They do hold values \u2014 ${hint}
2951
+ `;
2952
+ }
2939
2953
  function formatDatastoreSearchMarkdown(result, serverUrl, resourceId, offset, limit) {
2940
2954
  let markdown = `# DataStore Query Results
2941
2955
 
@@ -2949,18 +2963,19 @@ function formatDatastoreSearchMarkdown(result, serverUrl, resourceId, offset, li
2949
2963
  markdown += `**Returned**: ${result.records ? result.records.length : 0} records
2950
2964
 
2951
2965
  `;
2952
- if (result.fields && result.fields.length > 0) {
2966
+ const declaredFields = (result.fields || []).filter((f) => f.id !== "_full_text");
2967
+ if (declaredFields.length > 0) {
2953
2968
  markdown += `## Fields
2954
2969
 
2955
2970
  `;
2956
- markdown += result.fields.map((f) => `- **${f.id}** (${f.type})`).join("\n") + "\n\n";
2971
+ markdown += declaredFields.map((f) => `- **${f.id}** (${f.type})`).join("\n") + "\n\n";
2957
2972
  }
2958
2973
  if (result.records && result.records.length > 0) {
2959
2974
  markdown += `## Records
2960
2975
 
2961
2976
  `;
2962
- const fields = result.fields ? result.fields.map((f) => f.id).filter((id) => id !== "_id") : [];
2963
- const displayFields = fields.slice(0, 8);
2977
+ const fields = result.fields ? result.fields.map((f) => f.id).filter((id) => !isInternalField(id)) : [];
2978
+ const displayFields = fields.slice(0, MAX_TABLE_COLUMNS);
2964
2979
  markdown += `| ${displayFields.join(" | ")} |
2965
2980
  `;
2966
2981
  markdown += `| ${displayFields.map(() => "---").join(" | ")} |
@@ -2975,6 +2990,7 @@ function formatDatastoreSearchMarkdown(result, serverUrl, resourceId, offset, li
2975
2990
  markdown += `| ${values.join(" | ")} |
2976
2991
  `;
2977
2992
  }
2993
+ markdown += formatOmittedColumnsNote(fields, 'use the `fields` parameter to select them, or `response_format: "json"` to get every column.');
2978
2994
  if (result.records.length > 50) {
2979
2995
  markdown += `
2980
2996
  ... and ${result.records.length - 50} more records
@@ -2994,7 +3010,7 @@ function formatDatastoreSearchMarkdown(result, serverUrl, resourceId, offset, li
2994
3010
  }
2995
3011
  function formatDatastoreSqlMarkdown(result, serverUrl, sql) {
2996
3012
  const records = result.records || [];
2997
- const fieldIds = (result.fields?.map((field) => field.id) || Object.keys(records[0] || {})).filter((id) => id !== "_id");
3013
+ const fieldIds = (result.fields?.map((field) => field.id) || Object.keys(records[0] || {})).filter((id) => !isInternalField(id));
2998
3014
  let markdown = `# DataStore SQL Results
2999
3015
 
3000
3016
  `;
@@ -3005,17 +3021,18 @@ function formatDatastoreSqlMarkdown(result, serverUrl, sql) {
3005
3021
  markdown += `**Returned**: ${records.length} records
3006
3022
 
3007
3023
  `;
3008
- if (result.fields && result.fields.length > 0) {
3024
+ const declaredFields = (result.fields || []).filter((field) => field.id !== "_full_text");
3025
+ if (declaredFields.length > 0) {
3009
3026
  markdown += `## Fields
3010
3027
 
3011
3028
  `;
3012
- markdown += result.fields.map((field) => `- **${field.id}** (${field.type})`).join("\n") + "\n\n";
3029
+ markdown += declaredFields.map((field) => `- **${field.id}** (${field.type})`).join("\n") + "\n\n";
3013
3030
  }
3014
3031
  if (records.length > 0 && fieldIds.length > 0) {
3015
3032
  markdown += `## Records
3016
3033
 
3017
3034
  `;
3018
- const displayFields = fieldIds.slice(0, 8);
3035
+ const displayFields = fieldIds.slice(0, MAX_TABLE_COLUMNS);
3019
3036
  markdown += `| ${displayFields.join(" | ")} |
3020
3037
  `;
3021
3038
  markdown += `| ${displayFields.map(() => "---").join(" | ")} |
@@ -3030,6 +3047,7 @@ function formatDatastoreSqlMarkdown(result, serverUrl, sql) {
3030
3047
  markdown += `| ${values.join(" | ")} |
3031
3048
  `;
3032
3049
  }
3050
+ markdown += formatOmittedColumnsNote(fieldIds, 'name them explicitly in the SELECT clause, or use `response_format: "json"` to get every column.');
3033
3051
  if (records.length > 50) {
3034
3052
  markdown += `
3035
3053
  ... and ${records.length - 50} more records
@@ -3043,11 +3061,10 @@ function formatDatastoreSqlMarkdown(result, serverUrl, sql) {
3043
3061
  return markdown;
3044
3062
  }
3045
3063
  function compactDatastoreResult(result) {
3046
- const fields = (result.fields || []).filter((f) => f.id !== "_id");
3047
- const records = (result.records || []).map((record) => {
3048
- const { _id, ...rest } = record;
3049
- return rest;
3050
- });
3064
+ const fields = (result.fields || []).filter((f) => !isInternalField(f.id));
3065
+ const records = (result.records || []).map(
3066
+ (record) => Object.fromEntries(Object.entries(record).filter(([key]) => !isInternalField(key)))
3067
+ );
3051
3068
  return {
3052
3069
  resource_id: result.resource_id || null,
3053
3070
  fields,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aborruso/ckan-mcp-server",
3
- "version": "0.4.115",
3
+ "version": "0.4.116",
4
4
  "mcpName": "io.github.aborruso/ckan-mcp-server",
5
5
  "description": "MCP server for interacting with CKAN open data portals",
6
6
  "repository": {