@aborruso/ckan-mcp-server 0.4.30 → 0.4.32
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/AGENTS.md +4 -0
- package/LOG.md +11 -0
- package/README.md +11 -13
- package/dist/index.js +414 -148
- package/dist/worker.js +38 -38
- package/package.json +1 -1
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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**: ${
|
|
475
|
-
${
|
|
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(
|
|
792
|
-
structuredContent:
|
|
1140
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(enriched, null, 2)) }],
|
|
1141
|
+
structuredContent: enriched
|
|
793
1142
|
};
|
|
794
1143
|
}
|
|
795
|
-
|
|
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.
|
|
3349
|
+
version: "0.4.27"
|
|
3084
3350
|
});
|
|
3085
3351
|
}
|
|
3086
3352
|
function registerAll(server2) {
|