@aborruso/ckan-mcp-server 0.4.30 → 0.4.31

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/dist/index.js CHANGED
@@ -96,7 +96,100 @@ function getPortalApiUrlForHostname(hostname) {
96
96
  }
97
97
 
98
98
  // src/utils/http.ts
99
+ var loadZlib = /* @__PURE__ */ (() => {
100
+ let cached = null;
101
+ return async () => {
102
+ if (!cached) {
103
+ cached = (async () => {
104
+ try {
105
+ const mod = await import("node:zlib");
106
+ return mod;
107
+ } catch {
108
+ return null;
109
+ }
110
+ })();
111
+ }
112
+ return cached;
113
+ };
114
+ })();
115
+ function getHeaderValue(headers, name) {
116
+ if (!headers) {
117
+ return void 0;
118
+ }
119
+ const target = name.toLowerCase();
120
+ for (const [key, value] of Object.entries(headers)) {
121
+ if (key.toLowerCase() === target) {
122
+ return Array.isArray(value) ? value.join(",") : String(value);
123
+ }
124
+ }
125
+ return void 0;
126
+ }
127
+ function asBuffer(data) {
128
+ if (!data) {
129
+ return void 0;
130
+ }
131
+ if (typeof Buffer === "undefined") {
132
+ return void 0;
133
+ }
134
+ if (Buffer.isBuffer(data)) {
135
+ return data;
136
+ }
137
+ if (data instanceof ArrayBuffer) {
138
+ return Buffer.from(data);
139
+ }
140
+ if (ArrayBuffer.isView(data)) {
141
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
142
+ }
143
+ return void 0;
144
+ }
145
+ async function decodePossiblyCompressed(data, headers) {
146
+ if (data === null || data === void 0) {
147
+ return data;
148
+ }
149
+ if (typeof data === "object" && !asBuffer(data)) {
150
+ return data;
151
+ }
152
+ if (typeof data === "string") {
153
+ try {
154
+ return JSON.parse(data);
155
+ } catch {
156
+ return data;
157
+ }
158
+ }
159
+ const buffer = asBuffer(data);
160
+ if (!buffer) {
161
+ return data;
162
+ }
163
+ const encoding = getHeaderValue(headers, "content-encoding");
164
+ let decodedBuffer = buffer;
165
+ const zlib = await loadZlib();
166
+ try {
167
+ if (zlib) {
168
+ if (encoding?.includes("gzip")) {
169
+ decodedBuffer = zlib.gunzipSync(buffer);
170
+ } else if (encoding?.includes("br")) {
171
+ decodedBuffer = zlib.brotliDecompressSync(buffer);
172
+ } else if (encoding?.includes("deflate")) {
173
+ decodedBuffer = zlib.inflateSync(buffer);
174
+ } else if (buffer.length >= 2 && buffer[0] === 31 && buffer[1] === 139) {
175
+ decodedBuffer = zlib.gunzipSync(buffer);
176
+ }
177
+ }
178
+ } catch {
179
+ decodedBuffer = buffer;
180
+ }
181
+ const text = decodedBuffer.toString("utf-8").trim();
182
+ if (!text) {
183
+ return text;
184
+ }
185
+ try {
186
+ return JSON.parse(text);
187
+ } catch {
188
+ return text;
189
+ }
190
+ }
99
191
  async function makeCkanRequest(serverUrl, action, params = {}) {
192
+ const isNode = typeof process !== "undefined" && !!process.versions?.node;
100
193
  let resolvedServerUrl = serverUrl;
101
194
  try {
102
195
  const hostname = new URL(serverUrl).hostname;
@@ -109,29 +202,64 @@ async function makeCkanRequest(serverUrl, action, params = {}) {
109
202
  const baseUrl = resolvedServerUrl.replace(/\/$/, "");
110
203
  const url = `${baseUrl}/api/3/action/${action}`;
111
204
  try {
112
- const response = await axios.get(url, {
113
- params,
114
- timeout: 3e4,
115
- headers: {
116
- Accept: "application/json, text/plain, */*",
117
- "Accept-Language": "en-US,en;q=0.9,it;q=0.8",
118
- "Accept-Encoding": "gzip, deflate, br",
119
- Connection: "keep-alive",
120
- Referer: `${baseUrl}/`,
121
- "Sec-Fetch-Site": "same-origin",
122
- "Sec-Fetch-Mode": "navigate",
123
- "Sec-Fetch-Dest": "document",
124
- "Upgrade-Insecure-Requests": "1",
125
- "Sec-CH-UA": '"Chromium";v="120", "Not?A_Brand";v="24", "Google Chrome";v="120"',
126
- "Sec-CH-UA-Mobile": "?0",
127
- "Sec-CH-UA-Platform": '"Linux"',
128
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
205
+ let decodedData;
206
+ if (isNode) {
207
+ const response = await axios.get(url, {
208
+ params,
209
+ timeout: 3e4,
210
+ responseType: "arraybuffer",
211
+ headers: {
212
+ Accept: "application/json, text/plain, */*",
213
+ "Accept-Language": "en-US,en;q=0.9,it;q=0.8",
214
+ "Accept-Encoding": "gzip, deflate, br",
215
+ Connection: "keep-alive",
216
+ Referer: `${baseUrl}/`,
217
+ "Sec-Fetch-Site": "same-origin",
218
+ "Sec-Fetch-Mode": "navigate",
219
+ "Sec-Fetch-Dest": "document",
220
+ "Upgrade-Insecure-Requests": "1",
221
+ "Sec-CH-UA": '"Chromium";v="120", "Not?A_Brand";v="24", "Google Chrome";v="120"',
222
+ "Sec-CH-UA-Mobile": "?0",
223
+ "Sec-CH-UA-Platform": '"Linux"',
224
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
225
+ }
226
+ });
227
+ decodedData = await decodePossiblyCompressed(
228
+ response.data,
229
+ response.headers
230
+ );
231
+ } else {
232
+ const searchParams = new URLSearchParams();
233
+ for (const [key, value] of Object.entries(params)) {
234
+ if (value === void 0 || value === null) {
235
+ continue;
236
+ }
237
+ searchParams.set(key, String(value));
129
238
  }
130
- });
131
- if (response.data && response.data.success === true) {
132
- return response.data.result;
239
+ const fetchUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url;
240
+ const response = await fetch(fetchUrl, {
241
+ method: "GET",
242
+ headers: {
243
+ Accept: "application/json, text/plain, */*",
244
+ "Accept-Language": "en-US,en;q=0.9,it;q=0.8"
245
+ }
246
+ });
247
+ if (!response.ok) {
248
+ throw new Error(`CKAN API error (${response.status}): ${response.statusText}`);
249
+ }
250
+ const buffer = await response.arrayBuffer();
251
+ const headers = {};
252
+ response.headers.forEach((headerValue, headerKey) => {
253
+ headers[headerKey] = headerValue;
254
+ });
255
+ decodedData = await decodePossiblyCompressed(buffer, headers);
256
+ }
257
+ if (decodedData && decodedData.success === true) {
258
+ return decodedData.result;
133
259
  } else {
134
- throw new Error(`CKAN API returned success=false: ${JSON.stringify(response.data)}`);
260
+ throw new Error(
261
+ `CKAN API returned success=false: ${JSON.stringify(decodedData)}`
262
+ );
135
263
  }
136
264
  } catch (error) {
137
265
  if (axios.isAxiosError(error)) {
@@ -329,6 +457,194 @@ var scoreDatasetRelevance = (query, dataset, weights = DEFAULT_RELEVANCE_WEIGHTS
329
457
  breakdown.total = breakdown.title + breakdown.notes + breakdown.tags + breakdown.organization;
330
458
  return { total: breakdown.total, breakdown, terms };
331
459
  };
460
+ var parseAccessServices = (resource) => {
461
+ if (!resource || resource.access_services == null) return [];
462
+ const raw = resource.access_services;
463
+ if (Array.isArray(raw)) return raw;
464
+ if (typeof raw === "string" && raw.trim().length > 0) {
465
+ try {
466
+ const parsed = JSON.parse(raw);
467
+ return Array.isArray(parsed) ? parsed : [];
468
+ } catch {
469
+ return [];
470
+ }
471
+ }
472
+ return [];
473
+ };
474
+ var extractServiceEndpoints = (services) => {
475
+ const endpoints = [];
476
+ for (const service of services) {
477
+ const urls = service.endpoint_url;
478
+ if (Array.isArray(urls)) {
479
+ for (const url of urls) {
480
+ if (typeof url === "string" && url.trim().length > 0) endpoints.push(url.trim());
481
+ }
482
+ } else if (typeof urls === "string" && urls.trim().length > 0) {
483
+ endpoints.push(urls.trim());
484
+ }
485
+ }
486
+ return Array.from(new Set(endpoints));
487
+ };
488
+ var resolveDownloadUrl = (resource) => {
489
+ if (!resource) return null;
490
+ const downloadUrl = typeof resource.download_url === "string" ? resource.download_url.trim() : "";
491
+ const accessUrl = typeof resource.access_url === "string" ? resource.access_url.trim() : "";
492
+ const url = typeof resource.url === "string" ? resource.url.trim() : "";
493
+ return downloadUrl || accessUrl || url || null;
494
+ };
495
+ var enrichPackageShowResult = (result) => ({
496
+ ...result,
497
+ metadata_harvested_at: result.metadata_modified ?? null,
498
+ resources: Array.isArray(result.resources) ? result.resources.map((resource) => {
499
+ const accessServices = parseAccessServices(resource);
500
+ const accessEndpoints = extractServiceEndpoints(accessServices);
501
+ const effectiveDownloadUrl = resolveDownloadUrl(resource);
502
+ if (accessEndpoints.length === 0 && !effectiveDownloadUrl) return resource;
503
+ return {
504
+ ...resource,
505
+ ...accessEndpoints.length > 0 ? { access_service_endpoints: accessEndpoints } : {},
506
+ ...effectiveDownloadUrl ? { effective_download_url: effectiveDownloadUrl } : {}
507
+ };
508
+ }) : result.resources
509
+ });
510
+ var formatPackageShowMarkdown = (result, serverUrl) => {
511
+ let markdown = `# Dataset: ${result.title || result.name}
512
+
513
+ `;
514
+ markdown += `**Server**: ${serverUrl}
515
+ `;
516
+ markdown += `**Link**: ${getDatasetViewUrl(serverUrl, result)}
517
+
518
+ `;
519
+ markdown += `## Basic Information
520
+
521
+ `;
522
+ markdown += `- **ID**: \`${result.id}\`
523
+ `;
524
+ markdown += `- **Name**: \`${result.name}\`
525
+ `;
526
+ if (result.author) markdown += `- **Author**: ${result.author}
527
+ `;
528
+ if (result.author_email) markdown += `- **Author Email**: ${result.author_email}
529
+ `;
530
+ if (result.maintainer) markdown += `- **Maintainer**: ${result.maintainer}
531
+ `;
532
+ if (result.maintainer_email) markdown += `- **Maintainer Email**: ${result.maintainer_email}
533
+ `;
534
+ markdown += `- **License**: ${result.license_title || result.license_id || "Not specified"}
535
+ `;
536
+ markdown += `- **State**: ${result.state}
537
+ `;
538
+ markdown += `- **Created**: ${formatDate(result.metadata_created)}
539
+ `;
540
+ if (result.issued) {
541
+ markdown += `- **Issued**: ${formatDate(result.issued)}
542
+ `;
543
+ } else {
544
+ markdown += `- **Issued**: (missing in CKAN; downstream RDF may default to metadata_created, which is a record timestamp)
545
+ `;
546
+ }
547
+ if (result.modified) markdown += `- **Modified (Content)**: ${formatDate(result.modified)}
548
+ `;
549
+ markdown += `- **Metadata Modified (Record)**: ${formatDate(result.metadata_modified)}
550
+
551
+ `;
552
+ if (result.organization) {
553
+ markdown += `## Organization
554
+
555
+ `;
556
+ markdown += `- **Name**: ${result.organization.title || result.organization.name}
557
+ `;
558
+ markdown += `- **ID**: \`${result.organization.id}\`
559
+
560
+ `;
561
+ }
562
+ if (result.notes) {
563
+ markdown += `## Description
564
+
565
+ ${result.notes}
566
+
567
+ `;
568
+ }
569
+ if (result.tags && result.tags.length > 0) {
570
+ markdown += `## Tags
571
+
572
+ `;
573
+ markdown += result.tags.map((t) => `- ${t.name}`).join("\n") + "\n\n";
574
+ }
575
+ if (result.groups && result.groups.length > 0) {
576
+ markdown += `## Groups
577
+
578
+ `;
579
+ for (const group of result.groups) {
580
+ markdown += `- **${group.title || group.name}** (\`${group.name}\`)
581
+ `;
582
+ }
583
+ markdown += "\n";
584
+ }
585
+ if (result.resources && result.resources.length > 0) {
586
+ markdown += `## Resources (${result.resources.length})
587
+
588
+ `;
589
+ for (const resource of result.resources) {
590
+ markdown += `### ${resource.name || "Unnamed Resource"}
591
+
592
+ `;
593
+ markdown += `- **ID**: \`${resource.id}\`
594
+ `;
595
+ markdown += `- **Format**: ${resource.format || "Unknown"}
596
+ `;
597
+ if (resource.description) markdown += `- **Description**: ${resource.description}
598
+ `;
599
+ markdown += `- **URL**: ${resource.url}
600
+ `;
601
+ const accessServices = parseAccessServices(resource);
602
+ const accessEndpoints = extractServiceEndpoints(accessServices);
603
+ if (accessEndpoints.length > 0) {
604
+ markdown += `- **Access Service Endpoints**: ${accessEndpoints.join(", ")}
605
+ `;
606
+ }
607
+ const effectiveDownloadUrl = resolveDownloadUrl(resource);
608
+ if (effectiveDownloadUrl) {
609
+ markdown += `- **Effective Download URL**: ${effectiveDownloadUrl}
610
+ `;
611
+ }
612
+ if (resource.size) {
613
+ const formatBytes = (bytes) => {
614
+ if (!bytes || bytes === 0) return "0 B";
615
+ const k = 1024;
616
+ const sizes = ["B", "KB", "MB", "GB"];
617
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
618
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
619
+ };
620
+ markdown += `- **Size**: ${formatBytes(resource.size)}
621
+ `;
622
+ }
623
+ if (resource.mimetype) markdown += `- **MIME Type**: ${resource.mimetype}
624
+ `;
625
+ markdown += `- **Created**: ${formatDate(resource.created)}
626
+ `;
627
+ if (resource.last_modified) markdown += `- **Modified**: ${formatDate(resource.last_modified)}
628
+ `;
629
+ if (resource.datastore_active !== void 0) {
630
+ markdown += `- **DataStore**: ${resource.datastore_active ? "\u2705 Available" : "\u274C Not available"}
631
+ `;
632
+ }
633
+ markdown += "\n";
634
+ }
635
+ }
636
+ if (result.extras && result.extras.length > 0) {
637
+ markdown += `## Extra Fields
638
+
639
+ `;
640
+ for (const extra of result.extras) {
641
+ markdown += `- **${extra.key}**: ${extra.value}
642
+ `;
643
+ }
644
+ markdown += "\n";
645
+ }
646
+ return markdown;
647
+ };
332
648
  function registerPackageTools(server2) {
333
649
  server2.registerTool(
334
650
  "ckan_package_search",
@@ -344,6 +660,19 @@ Some CKAN portals use a restrictive default query parser that can break long OR
344
660
  For those portals, this tool may force the query into 'text:(...)' based on per-portal config.
345
661
  You can override with 'query_parser' to force or disable this behavior per request.
346
662
 
663
+ Important - Date field semantics:
664
+ - issued: publisher's content publish date when available (best proxy for "created/published")
665
+ - modified: publisher's content update date when available
666
+ - metadata_created: CKAN record creation timestamp (publish time on source portals,
667
+ harvest time on aggregators; fallback for "created" if issued missing)
668
+ - metadata_modified: CKAN record update timestamp (publish time on source portals,
669
+ harvest time on aggregators; use for "updated/modified in last X")
670
+
671
+ Content-recent helper:
672
+ - content_recent: if true, rewrites the query to use issued with a fallback to
673
+ metadata_created when issued is missing.
674
+ - content_recent_days: window for content_recent (default 30 days).
675
+
347
676
  Args:
348
677
  - server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it/opendata")
349
678
  - q (string): Search query using Solr syntax (default: "*:*" for all)
@@ -428,6 +757,8 @@ Examples:
428
757
  facet_field: z2.array(z2.string()).optional().describe("Fields to facet on"),
429
758
  facet_limit: z2.number().int().min(1).optional().default(50).describe("Maximum facet values per field"),
430
759
  include_drafts: z2.boolean().optional().default(false).describe("Include draft datasets"),
760
+ content_recent: z2.boolean().optional().default(false).describe("Use issued date with fallback to metadata_created for recent content"),
761
+ content_recent_days: z2.number().int().min(1).optional().default(30).describe("Day window for content_recent (default 30)"),
431
762
  query_parser: z2.enum(["default", "text"]).optional().describe("Override search parser ('text' forces text:(...) on non-fielded queries)"),
432
763
  response_format: ResponseFormatSchema
433
764
  }).strict(),
@@ -440,9 +771,18 @@ Examples:
440
771
  },
441
772
  async (params) => {
442
773
  try {
774
+ const userQuery = params.q;
775
+ let query = userQuery;
776
+ let effectiveSort = params.sort;
777
+ if (params.content_recent) {
778
+ const days = params.content_recent_days ?? 30;
779
+ const recentClause = `(issued:[NOW-${days}DAYS TO NOW]) OR (-issued:* AND metadata_created:[NOW-${days}DAYS TO NOW])`;
780
+ query = userQuery && userQuery !== "*:*" ? `(${userQuery}) AND (${recentClause})` : recentClause;
781
+ if (!effectiveSort) effectiveSort = "issued desc, metadata_created desc";
782
+ }
443
783
  const { effectiveQuery } = resolveSearchQuery(
444
784
  params.server_url,
445
- params.q,
785
+ query,
446
786
  params.query_parser
447
787
  );
448
788
  const apiParams = {
@@ -452,7 +792,7 @@ Examples:
452
792
  include_private: params.include_drafts
453
793
  };
454
794
  if (params.fq) apiParams.fq = params.fq;
455
- if (params.sort) apiParams.sort = params.sort;
795
+ if (effectiveSort) apiParams.sort = effectiveSort;
456
796
  if (params.facet_field && params.facet_field.length > 0) {
457
797
  apiParams["facet.field"] = JSON.stringify(params.facet_field);
458
798
  apiParams["facet.limit"] = params.facet_limit;
@@ -471,8 +811,10 @@ Examples:
471
811
  let markdown = `# CKAN Package Search Results
472
812
 
473
813
  **Server**: ${params.server_url}
474
- **Query**: ${params.q}
475
- ${effectiveQuery !== params.q ? `**Effective Query**: ${effectiveQuery}
814
+ **Query**: ${userQuery}
815
+ ${params.content_recent ? `**Content Recent**: last ${params.content_recent_days ?? 30} days (issued with metadata_created fallback)
816
+ ` : ""}
817
+ ${effectiveQuery !== userQuery ? `**Effective Query**: ${effectiveQuery}
476
818
  ` : ""}
477
819
  ${params.fq ? `**Filter**: ${params.fq}
478
820
  ` : ""}
@@ -751,6 +1093,12 @@ Examples:
751
1093
 
752
1094
  Returns full details including resources, organization, tags, and all metadata fields.
753
1095
 
1096
+ Notes:
1097
+ - metadata_modified is a CKAN record timestamp (publish time on source portals,
1098
+ harvest time on aggregators), not the content date.
1099
+ - issued/modified are content dates when provided by the publisher.
1100
+ - JSON output adds metadata_harvested_at (same as metadata_modified).
1101
+
754
1102
  Args:
755
1103
  - server_url (string): Base URL of CKAN server
756
1104
  - id (string): Dataset ID or name (machine-readable slug)
@@ -787,126 +1135,13 @@ Examples:
787
1135
  }
788
1136
  );
789
1137
  if (params.response_format === "json" /* JSON */) {
1138
+ const enriched = enrichPackageShowResult(result);
790
1139
  return {
791
- content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
792
- structuredContent: result
1140
+ content: [{ type: "text", text: truncateText(JSON.stringify(enriched, null, 2)) }],
1141
+ structuredContent: enriched
793
1142
  };
794
1143
  }
795
- let markdown = `# Dataset: ${result.title || result.name}
796
-
797
- `;
798
- markdown += `**Server**: ${params.server_url}
799
- `;
800
- markdown += `**Link**: ${getDatasetViewUrl(params.server_url, result)}
801
-
802
- `;
803
- markdown += `## Basic Information
804
-
805
- `;
806
- markdown += `- **ID**: \`${result.id}\`
807
- `;
808
- markdown += `- **Name**: \`${result.name}\`
809
- `;
810
- if (result.author) markdown += `- **Author**: ${result.author}
811
- `;
812
- if (result.author_email) markdown += `- **Author Email**: ${result.author_email}
813
- `;
814
- if (result.maintainer) markdown += `- **Maintainer**: ${result.maintainer}
815
- `;
816
- if (result.maintainer_email) markdown += `- **Maintainer Email**: ${result.maintainer_email}
817
- `;
818
- markdown += `- **License**: ${result.license_title || result.license_id || "Not specified"}
819
- `;
820
- markdown += `- **State**: ${result.state}
821
- `;
822
- markdown += `- **Created**: ${formatDate(result.metadata_created)}
823
- `;
824
- markdown += `- **Modified**: ${formatDate(result.metadata_modified)}
825
-
826
- `;
827
- if (result.organization) {
828
- markdown += `## Organization
829
-
830
- `;
831
- markdown += `- **Name**: ${result.organization.title || result.organization.name}
832
- `;
833
- markdown += `- **ID**: \`${result.organization.id}\`
834
-
835
- `;
836
- }
837
- if (result.notes) {
838
- markdown += `## Description
839
-
840
- ${result.notes}
841
-
842
- `;
843
- }
844
- if (result.tags && result.tags.length > 0) {
845
- markdown += `## Tags
846
-
847
- `;
848
- markdown += result.tags.map((t) => `- ${t.name}`).join("\n") + "\n\n";
849
- }
850
- if (result.groups && result.groups.length > 0) {
851
- markdown += `## Groups
852
-
853
- `;
854
- for (const group of result.groups) {
855
- markdown += `- **${group.title || group.name}** (\`${group.name}\`)
856
- `;
857
- }
858
- markdown += "\n";
859
- }
860
- if (result.resources && result.resources.length > 0) {
861
- markdown += `## Resources (${result.resources.length})
862
-
863
- `;
864
- for (const resource of result.resources) {
865
- markdown += `### ${resource.name || "Unnamed Resource"}
866
-
867
- `;
868
- markdown += `- **ID**: \`${resource.id}\`
869
- `;
870
- markdown += `- **Format**: ${resource.format || "Unknown"}
871
- `;
872
- if (resource.description) markdown += `- **Description**: ${resource.description}
873
- `;
874
- markdown += `- **URL**: ${resource.url}
875
- `;
876
- if (resource.size) {
877
- const formatBytes = (bytes) => {
878
- if (!bytes || bytes === 0) return "0 B";
879
- const k = 1024;
880
- const sizes = ["B", "KB", "MB", "GB"];
881
- const i = Math.floor(Math.log(bytes) / Math.log(k));
882
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
883
- };
884
- markdown += `- **Size**: ${formatBytes(resource.size)}
885
- `;
886
- }
887
- if (resource.mimetype) markdown += `- **MIME Type**: ${resource.mimetype}
888
- `;
889
- markdown += `- **Created**: ${formatDate(resource.created)}
890
- `;
891
- if (resource.last_modified) markdown += `- **Modified**: ${formatDate(resource.last_modified)}
892
- `;
893
- if (resource.datastore_active !== void 0) {
894
- markdown += `- **DataStore**: ${resource.datastore_active ? "\u2705 Available" : "\u274C Not available"}
895
- `;
896
- }
897
- markdown += "\n";
898
- }
899
- }
900
- if (result.extras && result.extras.length > 0) {
901
- markdown += `## Extra Fields
902
-
903
- `;
904
- for (const extra of result.extras) {
905
- markdown += `- **${extra.key}**: ${extra.value}
906
- `;
907
- }
908
- markdown += "\n";
909
- }
1144
+ const markdown = formatPackageShowMarkdown(result, params.server_url);
910
1145
  return {
911
1146
  content: [{ type: "text", text: truncateText(markdown) }]
912
1147
  };
@@ -2890,7 +3125,11 @@ ckan_package_search({
2890
3125
  q: "tags:${theme}",
2891
3126
  sort: "metadata_modified desc",
2892
3127
  rows: ${rows}
2893
- })`;
3128
+ })
3129
+
3130
+ Note: metadata_modified is a CKAN record timestamp (publish time on source portals,
3131
+ harvest time on aggregators). If the user asks for
3132
+ content publication dates, prefer issued (or modified) with explicit ISO ranges.`;
2894
3133
  var registerThemePrompt = (server2) => {
2895
3134
  server2.registerPrompt(
2896
3135
  THEME_PROMPT_NAME,
@@ -2931,7 +3170,11 @@ ckan_package_search({
2931
3170
  fq: "organization:<org-id>",
2932
3171
  sort: "metadata_modified desc",
2933
3172
  rows: ${rows}
2934
- })`;
3173
+ })
3174
+
3175
+ Note: metadata_modified is a CKAN record timestamp (publish time on source portals,
3176
+ harvest time on aggregators). If the user asks for
3177
+ content publication dates, prefer issued (or modified) with explicit ISO ranges.`;
2935
3178
  var registerOrganizationPrompt = (server2) => {
2936
3179
  server2.registerPrompt(
2937
3180
  ORGANIZATION_PROMPT_NAME,
@@ -2962,7 +3205,10 @@ ckan_package_search({
2962
3205
  rows: ${rows}
2963
3206
  })
2964
3207
 
2965
- Tip: try uppercase (CSV/JSON) or common variants if results are sparse.`;
3208
+ Tip: try uppercase (CSV/JSON) or common variants if results are sparse.
3209
+ Note: metadata_modified is a CKAN record timestamp (publish time on source portals,
3210
+ harvest time on aggregators). If the user asks for
3211
+ content publication dates, prefer issued (or modified) with explicit ISO ranges.`;
2966
3212
  var registerFormatPrompt = (server2) => {
2967
3213
  server2.registerPrompt(
2968
3214
  FORMAT_PROMPT_NAME,
@@ -2984,7 +3230,8 @@ import { z as z12 } from "zod";
2984
3230
  var RECENT_PROMPT_NAME = "ckan-recent-datasets";
2985
3231
  var buildRecentPromptText = (serverUrl, rows) => `# Guided search: recent datasets
2986
3232
 
2987
- Use \`ckan_package_search\` sorted by modification date:
3233
+ Use \`ckan_package_search\` sorted by metadata modification date (CKAN record updates,
3234
+ publish time on source portals, harvest time on aggregators):
2988
3235
 
2989
3236
  ckan_package_search({
2990
3237
  server_url: "${serverUrl}",
@@ -2993,13 +3240,32 @@ ckan_package_search({
2993
3240
  rows: ${rows}
2994
3241
  })
2995
3242
 
2996
- Optional: add a date filter for the last N days:
3243
+ Optional: add a date filter for the last N days (record metadata):
2997
3244
 
2998
3245
  ckan_package_search({
2999
3246
  server_url: "${serverUrl}",
3000
3247
  q: "metadata_modified:[NOW-30DAYS TO *]",
3001
3248
  sort: "metadata_modified desc",
3002
3249
  rows: ${rows}
3250
+ })
3251
+
3252
+ If the user asks for recent content publication dates, prefer \`issued\` (or \`modified\`)
3253
+ from the publisher and use explicit ISO date ranges:
3254
+
3255
+ ckan_package_search({
3256
+ server_url: "${serverUrl}",
3257
+ q: "issued:[2025-01-01T00:00:00Z TO *]",
3258
+ sort: "issued desc",
3259
+ rows: ${rows}
3260
+ })
3261
+
3262
+ Or use the helper flag to apply issued with a metadata_created fallback:
3263
+
3264
+ ckan_package_search({
3265
+ server_url: "${serverUrl}",
3266
+ content_recent: true,
3267
+ content_recent_days: 30,
3268
+ rows: ${rows}
3003
3269
  })`;
3004
3270
  var registerRecentPrompt = (server2) => {
3005
3271
  server2.registerPrompt(
@@ -3080,7 +3346,7 @@ var registerAllPrompts = (server2) => {
3080
3346
  function createServer() {
3081
3347
  return new McpServer({
3082
3348
  name: "ckan-mcp-server",
3083
- version: "0.4.26"
3349
+ version: "0.4.27"
3084
3350
  });
3085
3351
  }
3086
3352
  function registerAll(server2) {