@aborruso/ckan-mcp-server 0.4.5 → 0.4.7
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/EXAMPLES.md +19 -0
- package/LOG.md +13 -0
- package/README.md +47 -37
- package/dist/index.js +435 -13
- package/dist/worker.js +130 -74
- package/openspec/changes/add-ckan-analyze-dataset-structure/proposal.md +17 -0
- package/openspec/changes/add-ckan-analyze-dataset-structure/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-analyze-dataset-structure/tasks.md +6 -0
- package/openspec/changes/add-ckan-analyze-dataset-updates/proposal.md +17 -0
- package/openspec/changes/add-ckan-analyze-dataset-updates/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-analyze-dataset-updates/tasks.md +6 -0
- package/openspec/changes/add-ckan-audit-tool/proposal.md +17 -0
- package/openspec/changes/add-ckan-audit-tool/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-audit-tool/tasks.md +6 -0
- package/openspec/changes/add-ckan-dataset-insights/proposal.md +17 -0
- package/openspec/changes/add-ckan-dataset-insights/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-dataset-insights/tasks.md +6 -0
- package/openspec/changes/archive/2026-01-10-add-ckan-find-relevant-datasets/proposal.md +17 -0
- package/openspec/changes/archive/2026-01-10-add-ckan-find-relevant-datasets/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/archive/2026-01-10-add-ckan-find-relevant-datasets/tasks.md +6 -0
- package/openspec/changes/update-search-parser-config/proposal.md +13 -0
- package/openspec/changes/update-search-parser-config/specs/ckan-insights/spec.md +11 -0
- package/openspec/changes/update-search-parser-config/specs/ckan-search/spec.md +11 -0
- package/openspec/changes/update-search-parser-config/tasks.md +6 -0
- package/openspec/project.md +9 -7
- package/openspec/specs/ckan-insights/spec.md +19 -0
- package/openspec/specs/cloudflare-deployment/spec.md +344 -0
- package/package.json +1 -1
- /package/openspec/changes/{add-cloudflare-workers → archive/2026-01-10-add-cloudflare-workers}/design.md +0 -0
- /package/openspec/changes/{add-cloudflare-workers → archive/2026-01-10-add-cloudflare-workers}/proposal.md +0 -0
- /package/openspec/changes/{add-cloudflare-workers → archive/2026-01-10-add-cloudflare-workers}/specs/cloudflare-deployment/spec.md +0 -0
- /package/openspec/changes/{add-cloudflare-workers → archive/2026-01-10-add-cloudflare-workers}/tasks.md +0 -0
package/dist/index.js
CHANGED
|
@@ -90,42 +90,168 @@ var portals_default = {
|
|
|
90
90
|
"http://www.dati.gov.it/opendata",
|
|
91
91
|
"http://dati.gov.it/opendata"
|
|
92
92
|
],
|
|
93
|
+
search: {
|
|
94
|
+
force_text_field: true
|
|
95
|
+
},
|
|
93
96
|
dataset_view_url: "https://www.dati.gov.it/view-dataset/dataset?id={id}",
|
|
94
97
|
organization_view_url: "https://www.dati.gov.it/view-dataset?organization={name}"
|
|
95
98
|
}
|
|
96
99
|
],
|
|
97
100
|
defaults: {
|
|
98
101
|
dataset_view_url: "{server_url}/dataset/{name}",
|
|
99
|
-
organization_view_url: "{server_url}/organization/{name}"
|
|
102
|
+
organization_view_url: "{server_url}/organization/{name}",
|
|
103
|
+
search: {
|
|
104
|
+
force_text_field: false
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
};
|
|
102
108
|
|
|
103
|
-
// src/utils/
|
|
109
|
+
// src/utils/portal-config.ts
|
|
104
110
|
function normalizeUrl(url) {
|
|
105
111
|
return url.replace(/\/$/, "");
|
|
106
112
|
}
|
|
107
|
-
function
|
|
113
|
+
function getPortalConfig(serverUrl) {
|
|
108
114
|
const cleanServerUrl = normalizeUrl(serverUrl);
|
|
109
115
|
const portal = portals_default.portals.find((p) => {
|
|
110
116
|
const mainUrl = normalizeUrl(p.api_url);
|
|
111
117
|
const aliases = (p.api_url_aliases || []).map(normalizeUrl);
|
|
112
118
|
return mainUrl === cleanServerUrl || aliases.includes(cleanServerUrl);
|
|
113
119
|
});
|
|
120
|
+
return portal || null;
|
|
121
|
+
}
|
|
122
|
+
function getPortalSearchConfig(serverUrl) {
|
|
123
|
+
const portal = getPortalConfig(serverUrl);
|
|
124
|
+
const defaults = portals_default.defaults?.search || {};
|
|
125
|
+
return {
|
|
126
|
+
force_text_field: portal?.search?.force_text_field ?? defaults.force_text_field ?? false
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function normalizePortalUrl(serverUrl) {
|
|
130
|
+
return normalizeUrl(serverUrl);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/utils/url-generator.ts
|
|
134
|
+
function getDatasetViewUrl(serverUrl, pkg) {
|
|
135
|
+
const cleanServerUrl = normalizePortalUrl(serverUrl);
|
|
136
|
+
const portal = getPortalConfig(serverUrl);
|
|
114
137
|
const template = portal?.dataset_view_url || portals_default.defaults.dataset_view_url;
|
|
115
138
|
return template.replace("{server_url}", cleanServerUrl).replace("{id}", pkg.id).replace("{name}", pkg.name);
|
|
116
139
|
}
|
|
117
140
|
function getOrganizationViewUrl(serverUrl, org) {
|
|
118
|
-
const cleanServerUrl =
|
|
119
|
-
const portal =
|
|
120
|
-
const mainUrl = normalizeUrl(p.api_url);
|
|
121
|
-
const aliases = (p.api_url_aliases || []).map(normalizeUrl);
|
|
122
|
-
return mainUrl === cleanServerUrl || aliases.includes(cleanServerUrl);
|
|
123
|
-
});
|
|
141
|
+
const cleanServerUrl = normalizePortalUrl(serverUrl);
|
|
142
|
+
const portal = getPortalConfig(serverUrl);
|
|
124
143
|
const template = portal?.organization_view_url || portals_default.defaults.organization_view_url;
|
|
125
144
|
return template.replace("{server_url}", cleanServerUrl).replace("{id}", org.id).replace("{name}", org.name);
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
// src/utils/search.ts
|
|
148
|
+
var DEFAULT_SEARCH_QUERY = "*:*";
|
|
149
|
+
var FIELD_QUERY_PATTERN = /\b[a-zA-Z_][\w-]*:/;
|
|
150
|
+
function isFieldedQuery(query) {
|
|
151
|
+
return FIELD_QUERY_PATTERN.test(query);
|
|
152
|
+
}
|
|
153
|
+
function resolveSearchQuery(serverUrl, query, parserOverride) {
|
|
154
|
+
const portalSearchConfig = getPortalSearchConfig(serverUrl);
|
|
155
|
+
const portalForce = portalSearchConfig.force_text_field ?? false;
|
|
156
|
+
let forceTextField = false;
|
|
157
|
+
if (parserOverride === "text") {
|
|
158
|
+
forceTextField = true;
|
|
159
|
+
} else if (parserOverride === "default") {
|
|
160
|
+
forceTextField = false;
|
|
161
|
+
} else if (portalForce) {
|
|
162
|
+
const trimmedQuery = query.trim();
|
|
163
|
+
forceTextField = trimmedQuery !== DEFAULT_SEARCH_QUERY && !isFieldedQuery(trimmedQuery);
|
|
164
|
+
}
|
|
165
|
+
const effectiveQuery = forceTextField ? `text:(${query})` : query;
|
|
166
|
+
return { effectiveQuery, forcedTextField: forceTextField };
|
|
167
|
+
}
|
|
168
|
+
|
|
128
169
|
// src/tools/package.ts
|
|
170
|
+
var DEFAULT_RELEVANCE_WEIGHTS = {
|
|
171
|
+
title: 4,
|
|
172
|
+
notes: 2,
|
|
173
|
+
tags: 3,
|
|
174
|
+
organization: 1
|
|
175
|
+
};
|
|
176
|
+
var QUERY_STOPWORDS = /* @__PURE__ */ new Set([
|
|
177
|
+
"a",
|
|
178
|
+
"an",
|
|
179
|
+
"the",
|
|
180
|
+
"and",
|
|
181
|
+
"or",
|
|
182
|
+
"but",
|
|
183
|
+
"in",
|
|
184
|
+
"on",
|
|
185
|
+
"at",
|
|
186
|
+
"to",
|
|
187
|
+
"for",
|
|
188
|
+
"of",
|
|
189
|
+
"with",
|
|
190
|
+
"by",
|
|
191
|
+
"from",
|
|
192
|
+
"as",
|
|
193
|
+
"is",
|
|
194
|
+
"was",
|
|
195
|
+
"are",
|
|
196
|
+
"were",
|
|
197
|
+
"be",
|
|
198
|
+
"been",
|
|
199
|
+
"being",
|
|
200
|
+
"have",
|
|
201
|
+
"has",
|
|
202
|
+
"had",
|
|
203
|
+
"do",
|
|
204
|
+
"does",
|
|
205
|
+
"did",
|
|
206
|
+
"will",
|
|
207
|
+
"would",
|
|
208
|
+
"could",
|
|
209
|
+
"should",
|
|
210
|
+
"may",
|
|
211
|
+
"might",
|
|
212
|
+
"must",
|
|
213
|
+
"can",
|
|
214
|
+
"this",
|
|
215
|
+
"that",
|
|
216
|
+
"these",
|
|
217
|
+
"those"
|
|
218
|
+
]);
|
|
219
|
+
var extractQueryTerms = (query) => {
|
|
220
|
+
const matches = query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
221
|
+
const terms = matches.filter((term) => term.length > 1 && !QUERY_STOPWORDS.has(term));
|
|
222
|
+
return Array.from(new Set(terms));
|
|
223
|
+
};
|
|
224
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
225
|
+
var textMatchesTerms = (text, terms) => {
|
|
226
|
+
if (!text || terms.length === 0) return false;
|
|
227
|
+
const normalized = text.toLowerCase().replace(/_/g, " ");
|
|
228
|
+
return terms.some((term) => new RegExp(`\\b${escapeRegExp(term)}\\b`, "i").test(normalized));
|
|
229
|
+
};
|
|
230
|
+
var scoreTextField = (text, terms, weight) => {
|
|
231
|
+
return textMatchesTerms(text, terms) ? weight : 0;
|
|
232
|
+
};
|
|
233
|
+
var scoreDatasetRelevance = (query, dataset, weights = DEFAULT_RELEVANCE_WEIGHTS) => {
|
|
234
|
+
const terms = extractQueryTerms(query);
|
|
235
|
+
const titleText = dataset.title || dataset.name || "";
|
|
236
|
+
const notesText = dataset.notes || "";
|
|
237
|
+
const orgText = dataset.organization?.title || dataset.organization?.name || dataset.owner_org || "";
|
|
238
|
+
const breakdown = {
|
|
239
|
+
title: scoreTextField(titleText, terms, weights.title),
|
|
240
|
+
notes: scoreTextField(notesText, terms, weights.notes),
|
|
241
|
+
tags: 0,
|
|
242
|
+
organization: scoreTextField(orgText, terms, weights.organization),
|
|
243
|
+
total: 0
|
|
244
|
+
};
|
|
245
|
+
if (Array.isArray(dataset.tags) && dataset.tags.length > 0 && terms.length > 0) {
|
|
246
|
+
const tagMatch = dataset.tags.some((tag) => {
|
|
247
|
+
const tagValue = typeof tag === "string" ? tag : tag?.name;
|
|
248
|
+
return textMatchesTerms(tagValue, terms);
|
|
249
|
+
});
|
|
250
|
+
breakdown.tags = tagMatch ? weights.tags : 0;
|
|
251
|
+
}
|
|
252
|
+
breakdown.total = breakdown.title + breakdown.notes + breakdown.tags + breakdown.organization;
|
|
253
|
+
return { total: breakdown.total, breakdown, terms };
|
|
254
|
+
};
|
|
129
255
|
function registerPackageTools(server2) {
|
|
130
256
|
server2.registerTool(
|
|
131
257
|
"ckan_package_search",
|
|
@@ -136,8 +262,13 @@ function registerPackageTools(server2) {
|
|
|
136
262
|
Supports full Solr search capabilities including filters, facets, and sorting.
|
|
137
263
|
Use this to discover datasets matching specific criteria.
|
|
138
264
|
|
|
265
|
+
Note on parser behavior:
|
|
266
|
+
Some CKAN portals use a restrictive default query parser that can break long OR queries.
|
|
267
|
+
For those portals, this tool may force the query into 'text:(...)' based on per-portal config.
|
|
268
|
+
You can override with 'query_parser' to force or disable this behavior per request.
|
|
269
|
+
|
|
139
270
|
Args:
|
|
140
|
-
- server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it")
|
|
271
|
+
- server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it/opendata")
|
|
141
272
|
- q (string): Search query using Solr syntax (default: "*:*" for all)
|
|
142
273
|
- fq (string): Filter query (e.g., "organization:comune-palermo")
|
|
143
274
|
- rows (number): Number of results to return (default: 10, max: 1000)
|
|
@@ -146,6 +277,7 @@ Args:
|
|
|
146
277
|
- facet_field (array): Fields to facet on (e.g., ["organization", "tags"])
|
|
147
278
|
- facet_limit (number): Max facet values per field (default: 50)
|
|
148
279
|
- include_drafts (boolean): Include draft datasets (default: false)
|
|
280
|
+
- query_parser ('default' | 'text'): Override search parser behavior
|
|
149
281
|
- response_format ('markdown' | 'json'): Output format
|
|
150
282
|
|
|
151
283
|
Returns:
|
|
@@ -215,6 +347,7 @@ Examples:
|
|
|
215
347
|
facet_field: z2.array(z2.string()).optional().describe("Fields to facet on"),
|
|
216
348
|
facet_limit: z2.number().int().min(1).optional().default(50).describe("Maximum facet values per field"),
|
|
217
349
|
include_drafts: z2.boolean().optional().default(false).describe("Include draft datasets"),
|
|
350
|
+
query_parser: z2.enum(["default", "text"]).optional().describe("Override search parser ('text' forces text:(...) on non-fielded queries)"),
|
|
218
351
|
response_format: ResponseFormatSchema
|
|
219
352
|
}).strict(),
|
|
220
353
|
annotations: {
|
|
@@ -226,8 +359,13 @@ Examples:
|
|
|
226
359
|
},
|
|
227
360
|
async (params) => {
|
|
228
361
|
try {
|
|
362
|
+
const { effectiveQuery } = resolveSearchQuery(
|
|
363
|
+
params.server_url,
|
|
364
|
+
params.q,
|
|
365
|
+
params.query_parser
|
|
366
|
+
);
|
|
229
367
|
const apiParams = {
|
|
230
|
-
q:
|
|
368
|
+
q: effectiveQuery,
|
|
231
369
|
rows: params.rows,
|
|
232
370
|
start: params.start,
|
|
233
371
|
include_private: params.include_drafts
|
|
@@ -253,6 +391,8 @@ Examples:
|
|
|
253
391
|
|
|
254
392
|
**Server**: ${params.server_url}
|
|
255
393
|
**Query**: ${params.q}
|
|
394
|
+
${effectiveQuery !== params.q ? `**Effective Query**: ${effectiveQuery}
|
|
395
|
+
` : ""}
|
|
256
396
|
${params.fq ? `**Filter**: ${params.fq}
|
|
257
397
|
` : ""}
|
|
258
398
|
**Total Results**: ${result.count}
|
|
@@ -335,6 +475,184 @@ ${params.fq ? `**Filter**: ${params.fq}
|
|
|
335
475
|
}
|
|
336
476
|
}
|
|
337
477
|
);
|
|
478
|
+
server2.registerTool(
|
|
479
|
+
"ckan_find_relevant_datasets",
|
|
480
|
+
{
|
|
481
|
+
title: "Find Relevant CKAN Datasets",
|
|
482
|
+
description: `Find and rank datasets by relevance to a query using weighted fields.
|
|
483
|
+
|
|
484
|
+
Uses package_search for discovery and applies a local scoring model.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
- server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it/opendata")
|
|
488
|
+
- query (string): Search query text
|
|
489
|
+
- limit (number): Number of datasets to return (default: 10)
|
|
490
|
+
- weights (object): Optional weights for title/notes/tags/organization
|
|
491
|
+
- query_parser ('default' | 'text'): Override search parser behavior
|
|
492
|
+
- response_format ('markdown' | 'json'): Output format
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Ranked datasets with relevance scores and breakdowns
|
|
496
|
+
|
|
497
|
+
Examples:
|
|
498
|
+
- { server_url: "https://dati.gov.it/opendata", query: "mobilit\xE0" }
|
|
499
|
+
- { server_url: "...", query: "trasporti", limit: 5, weights: { title: 5, notes: 2 } }`,
|
|
500
|
+
inputSchema: z2.object({
|
|
501
|
+
server_url: z2.string().url().describe("Base URL of the CKAN server (e.g., https://dati.gov.it/opendata)"),
|
|
502
|
+
query: z2.string().min(2).describe("Search query text"),
|
|
503
|
+
limit: z2.number().int().min(1).max(50).optional().default(10).describe("Number of datasets to return"),
|
|
504
|
+
weights: z2.object({
|
|
505
|
+
title: z2.number().min(0).optional(),
|
|
506
|
+
notes: z2.number().min(0).optional(),
|
|
507
|
+
tags: z2.number().min(0).optional(),
|
|
508
|
+
organization: z2.number().min(0).optional()
|
|
509
|
+
}).optional().describe("Optional weights per field"),
|
|
510
|
+
query_parser: z2.enum(["default", "text"]).optional().describe("Override search parser ('text' forces text:(...) on non-fielded queries)"),
|
|
511
|
+
response_format: ResponseFormatSchema
|
|
512
|
+
}).strict(),
|
|
513
|
+
annotations: {
|
|
514
|
+
readOnlyHint: true,
|
|
515
|
+
destructiveHint: false,
|
|
516
|
+
idempotentHint: true,
|
|
517
|
+
openWorldHint: true
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
async (params) => {
|
|
521
|
+
try {
|
|
522
|
+
const weights = {
|
|
523
|
+
...DEFAULT_RELEVANCE_WEIGHTS,
|
|
524
|
+
...params.weights ?? {}
|
|
525
|
+
};
|
|
526
|
+
const rows = Math.min(Math.max(params.limit * 5, params.limit), 100);
|
|
527
|
+
const { effectiveQuery } = resolveSearchQuery(
|
|
528
|
+
params.server_url,
|
|
529
|
+
params.query,
|
|
530
|
+
params.query_parser
|
|
531
|
+
);
|
|
532
|
+
const searchResult = await makeCkanRequest(
|
|
533
|
+
params.server_url,
|
|
534
|
+
"package_search",
|
|
535
|
+
{
|
|
536
|
+
q: effectiveQuery,
|
|
537
|
+
rows,
|
|
538
|
+
start: 0
|
|
539
|
+
}
|
|
540
|
+
);
|
|
541
|
+
const scored = (searchResult.results || []).map((dataset) => {
|
|
542
|
+
const { total, breakdown } = scoreDatasetRelevance(
|
|
543
|
+
params.query,
|
|
544
|
+
dataset,
|
|
545
|
+
weights
|
|
546
|
+
);
|
|
547
|
+
return {
|
|
548
|
+
dataset,
|
|
549
|
+
score: total,
|
|
550
|
+
breakdown
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
scored.sort((a, b) => b.score - a.score);
|
|
554
|
+
const top = scored.slice(0, params.limit).map((item) => {
|
|
555
|
+
const dataset = item.dataset;
|
|
556
|
+
return {
|
|
557
|
+
id: dataset.id,
|
|
558
|
+
name: dataset.name,
|
|
559
|
+
title: dataset.title || dataset.name,
|
|
560
|
+
organization: dataset.organization?.title || dataset.organization?.name || dataset.owner_org,
|
|
561
|
+
tags: Array.isArray(dataset.tags) ? dataset.tags.map((tag) => tag.name) : [],
|
|
562
|
+
metadata_modified: dataset.metadata_modified,
|
|
563
|
+
score: item.score,
|
|
564
|
+
breakdown: item.breakdown
|
|
565
|
+
};
|
|
566
|
+
});
|
|
567
|
+
const payload = {
|
|
568
|
+
query: params.query,
|
|
569
|
+
terms: extractQueryTerms(params.query),
|
|
570
|
+
weights,
|
|
571
|
+
total_results: searchResult.count ?? 0,
|
|
572
|
+
returned: top.length,
|
|
573
|
+
results: top
|
|
574
|
+
};
|
|
575
|
+
if (params.response_format === "json" /* JSON */) {
|
|
576
|
+
return {
|
|
577
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(payload, null, 2)) }],
|
|
578
|
+
structuredContent: payload
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
let markdown = `# Relevant CKAN Datasets
|
|
582
|
+
|
|
583
|
+
`;
|
|
584
|
+
markdown += `**Server**: ${params.server_url}
|
|
585
|
+
`;
|
|
586
|
+
markdown += `**Query**: ${params.query}
|
|
587
|
+
`;
|
|
588
|
+
markdown += `**Terms**: ${payload.terms.length > 0 ? payload.terms.join(", ") : "n/a"}
|
|
589
|
+
`;
|
|
590
|
+
markdown += `**Total Results**: ${payload.total_results}
|
|
591
|
+
`;
|
|
592
|
+
markdown += `**Returned**: ${payload.returned}
|
|
593
|
+
|
|
594
|
+
`;
|
|
595
|
+
markdown += `## Weights
|
|
596
|
+
|
|
597
|
+
`;
|
|
598
|
+
markdown += `- **Title**: ${weights.title}
|
|
599
|
+
`;
|
|
600
|
+
markdown += `- **Notes**: ${weights.notes}
|
|
601
|
+
`;
|
|
602
|
+
markdown += `- **Tags**: ${weights.tags}
|
|
603
|
+
`;
|
|
604
|
+
markdown += `- **Organization**: ${weights.organization}
|
|
605
|
+
|
|
606
|
+
`;
|
|
607
|
+
if (top.length === 0) {
|
|
608
|
+
markdown += "No datasets matched the query terms.\n";
|
|
609
|
+
} else {
|
|
610
|
+
markdown += `## Results
|
|
611
|
+
|
|
612
|
+
`;
|
|
613
|
+
markdown += `| Rank | Dataset | Score | Title | Org | Tags |
|
|
614
|
+
`;
|
|
615
|
+
markdown += `| --- | --- | --- | --- | --- | --- |
|
|
616
|
+
`;
|
|
617
|
+
top.forEach((dataset, index) => {
|
|
618
|
+
const tags = dataset.tags.slice(0, 3).join(", ");
|
|
619
|
+
markdown += `| ${index + 1} | ${dataset.name} | ${dataset.score} | ${dataset.title} | ${dataset.organization || "-"} | ${tags || "-"} |
|
|
620
|
+
`;
|
|
621
|
+
});
|
|
622
|
+
markdown += `
|
|
623
|
+
### Score Breakdown
|
|
624
|
+
|
|
625
|
+
`;
|
|
626
|
+
top.forEach((dataset, index) => {
|
|
627
|
+
markdown += `**${index + 1}. ${dataset.title}**
|
|
628
|
+
`;
|
|
629
|
+
markdown += `- Title: ${dataset.breakdown.title}
|
|
630
|
+
`;
|
|
631
|
+
markdown += `- Notes: ${dataset.breakdown.notes}
|
|
632
|
+
`;
|
|
633
|
+
markdown += `- Tags: ${dataset.breakdown.tags}
|
|
634
|
+
`;
|
|
635
|
+
markdown += `- Organization: ${dataset.breakdown.organization}
|
|
636
|
+
`;
|
|
637
|
+
markdown += `- Total: ${dataset.breakdown.total}
|
|
638
|
+
|
|
639
|
+
`;
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
content: [{ type: "text", text: truncateText(markdown) }]
|
|
644
|
+
};
|
|
645
|
+
} catch (error) {
|
|
646
|
+
return {
|
|
647
|
+
content: [{
|
|
648
|
+
type: "text",
|
|
649
|
+
text: `Error ranking datasets: ${error instanceof Error ? error.message : String(error)}`
|
|
650
|
+
}],
|
|
651
|
+
isError: true
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
);
|
|
338
656
|
server2.registerTool(
|
|
339
657
|
"ckan_package_show",
|
|
340
658
|
{
|
|
@@ -353,7 +671,7 @@ Returns:
|
|
|
353
671
|
Complete dataset object with all metadata and resources
|
|
354
672
|
|
|
355
673
|
Examples:
|
|
356
|
-
- { server_url: "https://dati.gov.it", id: "dataset-name" }
|
|
674
|
+
- { server_url: "https://dati.gov.it/opendata", id: "dataset-name" }
|
|
357
675
|
- { server_url: "...", id: "abc-123-def", include_tracking: true }`,
|
|
358
676
|
inputSchema: z2.object({
|
|
359
677
|
server_url: z2.string().url().describe("Base URL of the CKAN server"),
|
|
@@ -998,6 +1316,110 @@ Examples:
|
|
|
998
1316
|
}
|
|
999
1317
|
}
|
|
1000
1318
|
);
|
|
1319
|
+
server2.registerTool(
|
|
1320
|
+
"ckan_datastore_search_sql",
|
|
1321
|
+
{
|
|
1322
|
+
title: "Search CKAN DataStore with SQL",
|
|
1323
|
+
description: `Run SQL queries on a CKAN DataStore resource.
|
|
1324
|
+
|
|
1325
|
+
This endpoint is only available on CKAN portals with DataStore enabled and SQL access exposed.
|
|
1326
|
+
|
|
1327
|
+
Args:
|
|
1328
|
+
- server_url (string): Base URL of CKAN server
|
|
1329
|
+
- sql (string): SQL query (e.g., SELECT * FROM "resource_id" LIMIT 10)
|
|
1330
|
+
- response_format ('markdown' | 'json'): Output format
|
|
1331
|
+
|
|
1332
|
+
Returns:
|
|
1333
|
+
SQL query results from DataStore
|
|
1334
|
+
|
|
1335
|
+
Examples:
|
|
1336
|
+
- { server_url: "...", sql: "SELECT * FROM "abc-123" LIMIT 10" }
|
|
1337
|
+
- { server_url: "...", sql: "SELECT COUNT(*) AS total FROM "abc-123"" }`,
|
|
1338
|
+
inputSchema: z4.object({
|
|
1339
|
+
server_url: z4.string().url(),
|
|
1340
|
+
sql: z4.string().min(1),
|
|
1341
|
+
response_format: ResponseFormatSchema
|
|
1342
|
+
}).strict(),
|
|
1343
|
+
annotations: {
|
|
1344
|
+
readOnlyHint: true,
|
|
1345
|
+
destructiveHint: false,
|
|
1346
|
+
idempotentHint: true,
|
|
1347
|
+
openWorldHint: false
|
|
1348
|
+
}
|
|
1349
|
+
},
|
|
1350
|
+
async (params) => {
|
|
1351
|
+
try {
|
|
1352
|
+
const result = await makeCkanRequest(
|
|
1353
|
+
params.server_url,
|
|
1354
|
+
"datastore_search_sql",
|
|
1355
|
+
{ sql: params.sql }
|
|
1356
|
+
);
|
|
1357
|
+
if (params.response_format === "json" /* JSON */) {
|
|
1358
|
+
return {
|
|
1359
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
|
|
1360
|
+
structuredContent: result
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
const records = result.records || [];
|
|
1364
|
+
const fieldIds = result.fields?.map((field) => field.id) || Object.keys(records[0] || {});
|
|
1365
|
+
let markdown = `# DataStore SQL Results
|
|
1366
|
+
|
|
1367
|
+
`;
|
|
1368
|
+
markdown += `**Server**: ${params.server_url}
|
|
1369
|
+
`;
|
|
1370
|
+
markdown += `**SQL**: \`${params.sql}\`
|
|
1371
|
+
`;
|
|
1372
|
+
markdown += `**Returned**: ${records.length} records
|
|
1373
|
+
|
|
1374
|
+
`;
|
|
1375
|
+
if (result.fields && result.fields.length > 0) {
|
|
1376
|
+
markdown += `## Fields
|
|
1377
|
+
|
|
1378
|
+
`;
|
|
1379
|
+
markdown += result.fields.map((field) => `- **${field.id}** (${field.type})`).join("\n") + "\n\n";
|
|
1380
|
+
}
|
|
1381
|
+
if (records.length > 0 && fieldIds.length > 0) {
|
|
1382
|
+
markdown += `## Records
|
|
1383
|
+
|
|
1384
|
+
`;
|
|
1385
|
+
const displayFields = fieldIds.slice(0, 8);
|
|
1386
|
+
markdown += `| ${displayFields.join(" | ")} |
|
|
1387
|
+
`;
|
|
1388
|
+
markdown += `| ${displayFields.map(() => "---").join(" | ")} |
|
|
1389
|
+
`;
|
|
1390
|
+
for (const record of records.slice(0, 50)) {
|
|
1391
|
+
const values = displayFields.map((field) => {
|
|
1392
|
+
const value = record[field];
|
|
1393
|
+
if (value === null || value === void 0) return "-";
|
|
1394
|
+
const text = String(value);
|
|
1395
|
+
return text.length > 50 ? text.substring(0, 47) + "..." : text;
|
|
1396
|
+
});
|
|
1397
|
+
markdown += `| ${values.join(" | ")} |
|
|
1398
|
+
`;
|
|
1399
|
+
}
|
|
1400
|
+
if (records.length > 50) {
|
|
1401
|
+
markdown += `
|
|
1402
|
+
... and ${records.length - 50} more records
|
|
1403
|
+
`;
|
|
1404
|
+
}
|
|
1405
|
+
markdown += "\n";
|
|
1406
|
+
} else {
|
|
1407
|
+
markdown += "No records returned by the SQL query.\n";
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
content: [{ type: "text", text: truncateText(markdown) }]
|
|
1411
|
+
};
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
return {
|
|
1414
|
+
content: [{
|
|
1415
|
+
type: "text",
|
|
1416
|
+
text: `Error querying DataStore SQL: ${error instanceof Error ? error.message : String(error)}`
|
|
1417
|
+
}],
|
|
1418
|
+
isError: true
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
);
|
|
1001
1423
|
}
|
|
1002
1424
|
|
|
1003
1425
|
// src/tools/status.ts
|
|
@@ -1727,7 +2149,7 @@ function registerAllResources(server2) {
|
|
|
1727
2149
|
function createServer() {
|
|
1728
2150
|
return new McpServer({
|
|
1729
2151
|
name: "ckan-mcp-server",
|
|
1730
|
-
version: "0.4.
|
|
2152
|
+
version: "0.4.7"
|
|
1731
2153
|
});
|
|
1732
2154
|
}
|
|
1733
2155
|
function registerAll(server2) {
|