@aborruso/ckan-mcp-server 0.3.1

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 (43) hide show
  1. package/.claude/commands/openspec/apply.md +23 -0
  2. package/.claude/commands/openspec/archive.md +27 -0
  3. package/.claude/commands/openspec/proposal.md +28 -0
  4. package/.claude/settings.local.json +31 -0
  5. package/.gemini/commands/openspec/apply.toml +21 -0
  6. package/.gemini/commands/openspec/archive.toml +25 -0
  7. package/.gemini/commands/openspec/proposal.toml +26 -0
  8. package/.mcp.json +12 -0
  9. package/.opencode/command/openspec-apply.md +24 -0
  10. package/.opencode/command/openspec-archive.md +27 -0
  11. package/.opencode/command/openspec-proposal.md +29 -0
  12. package/AGENTS.md +18 -0
  13. package/CLAUDE.md +320 -0
  14. package/EXAMPLES.md +707 -0
  15. package/LICENSE.txt +21 -0
  16. package/LOG.md +154 -0
  17. package/PRD.md +912 -0
  18. package/README.md +468 -0
  19. package/REFACTORING.md +237 -0
  20. package/dist/index.js +1277 -0
  21. package/openspec/AGENTS.md +456 -0
  22. package/openspec/changes/archive/2026-01-08-add-mcp-resources/design.md +115 -0
  23. package/openspec/changes/archive/2026-01-08-add-mcp-resources/proposal.md +52 -0
  24. package/openspec/changes/archive/2026-01-08-add-mcp-resources/specs/mcp-resources/spec.md +92 -0
  25. package/openspec/changes/archive/2026-01-08-add-mcp-resources/tasks.md +56 -0
  26. package/openspec/changes/archive/2026-01-08-expand-test-coverage-specs/design.md +355 -0
  27. package/openspec/changes/archive/2026-01-08-expand-test-coverage-specs/proposal.md +161 -0
  28. package/openspec/changes/archive/2026-01-08-expand-test-coverage-specs/tasks.md +162 -0
  29. package/openspec/changes/archive/2026-01-08-translate-project-to-english/proposal.md +115 -0
  30. package/openspec/changes/archive/2026-01-08-translate-project-to-english/specs/documentation-language/spec.md +32 -0
  31. package/openspec/changes/archive/2026-01-08-translate-project-to-english/tasks.md +115 -0
  32. package/openspec/changes/archive/add-automated-tests/design.md +324 -0
  33. package/openspec/changes/archive/add-automated-tests/proposal.md +167 -0
  34. package/openspec/changes/archive/add-automated-tests/specs/automated-testing/spec.md +143 -0
  35. package/openspec/changes/archive/add-automated-tests/tasks.md +132 -0
  36. package/openspec/project.md +113 -0
  37. package/openspec/specs/documentation-language/spec.md +32 -0
  38. package/openspec/specs/mcp-resources/spec.md +94 -0
  39. package/package.json +46 -0
  40. package/spunti.md +19 -0
  41. package/tasks/todo.md +124 -0
  42. package/test-urls.js +18 -0
  43. package/tmp/test-org-search.js +55 -0
package/dist/index.js ADDED
@@ -0,0 +1,1277 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ function createServer() {
6
+ return new McpServer({
7
+ name: "ckan-mcp-server",
8
+ version: "0.1.0"
9
+ });
10
+ }
11
+
12
+ // src/tools/package.ts
13
+ import { z as z2 } from "zod";
14
+
15
+ // src/types.ts
16
+ import { z } from "zod";
17
+ var ResponseFormat = /* @__PURE__ */ ((ResponseFormat2) => {
18
+ ResponseFormat2["MARKDOWN"] = "markdown";
19
+ ResponseFormat2["JSON"] = "json";
20
+ return ResponseFormat2;
21
+ })(ResponseFormat || {});
22
+ var ResponseFormatSchema = z.nativeEnum(ResponseFormat).default("markdown" /* MARKDOWN */).describe("Output format: 'markdown' for human-readable or 'json' for machine-readable");
23
+ var CHARACTER_LIMIT = 5e4;
24
+
25
+ // src/utils/http.ts
26
+ import axios from "axios";
27
+ async function makeCkanRequest(serverUrl, action, params = {}) {
28
+ const baseUrl = serverUrl.replace(/\/$/, "");
29
+ const url = `${baseUrl}/api/3/action/${action}`;
30
+ try {
31
+ const response = await axios.get(url, {
32
+ params,
33
+ timeout: 3e4,
34
+ headers: {
35
+ "User-Agent": "CKAN-MCP-Server/1.0"
36
+ }
37
+ });
38
+ if (response.data && response.data.success === true) {
39
+ return response.data.result;
40
+ } else {
41
+ throw new Error(`CKAN API returned success=false: ${JSON.stringify(response.data)}`);
42
+ }
43
+ } catch (error) {
44
+ if (axios.isAxiosError(error)) {
45
+ const axiosError = error;
46
+ if (axiosError.response) {
47
+ const status = axiosError.response.status;
48
+ const data = axiosError.response.data;
49
+ const errorMsg = data?.error?.message || data?.error || "Unknown error";
50
+ throw new Error(`CKAN API error (${status}): ${errorMsg}`);
51
+ } else if (axiosError.code === "ECONNABORTED") {
52
+ throw new Error(`Request timeout connecting to ${serverUrl}`);
53
+ } else if (axiosError.code === "ENOTFOUND") {
54
+ throw new Error(`Server not found: ${serverUrl}`);
55
+ } else {
56
+ throw new Error(`Network error: ${axiosError.message}`);
57
+ }
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ // src/utils/formatting.ts
64
+ function truncateText(text, limit = CHARACTER_LIMIT) {
65
+ if (text.length <= limit) {
66
+ return text;
67
+ }
68
+ return text.substring(0, limit) + `
69
+
70
+ ... [Response truncated at ${limit} characters]`;
71
+ }
72
+ function formatDate(dateStr) {
73
+ try {
74
+ return new Date(dateStr).toLocaleString("it-IT");
75
+ } catch {
76
+ return dateStr;
77
+ }
78
+ }
79
+
80
+ // src/portals.json
81
+ var portals_default = {
82
+ portals: [
83
+ {
84
+ id: "dati-gov-it",
85
+ name: "dati.gov.it",
86
+ api_url: "https://www.dati.gov.it/opendata",
87
+ api_url_aliases: [
88
+ "https://dati.gov.it/opendata",
89
+ "http://www.dati.gov.it/opendata",
90
+ "http://dati.gov.it/opendata"
91
+ ],
92
+ dataset_view_url: "https://www.dati.gov.it/view-dataset/dataset?id={id}",
93
+ organization_view_url: "https://www.dati.gov.it/view-dataset?organization={name}"
94
+ }
95
+ ],
96
+ defaults: {
97
+ dataset_view_url: "{server_url}/dataset/{name}",
98
+ organization_view_url: "{server_url}/organization/{name}"
99
+ }
100
+ };
101
+
102
+ // src/utils/url-generator.ts
103
+ function normalizeUrl(url) {
104
+ return url.replace(/\/$/, "");
105
+ }
106
+ function getDatasetViewUrl(serverUrl, pkg) {
107
+ const cleanServerUrl = normalizeUrl(serverUrl);
108
+ const portal = portals_default.portals.find((p) => {
109
+ const mainUrl = normalizeUrl(p.api_url);
110
+ const aliases = (p.api_url_aliases || []).map(normalizeUrl);
111
+ return mainUrl === cleanServerUrl || aliases.includes(cleanServerUrl);
112
+ });
113
+ const template = portal?.dataset_view_url || portals_default.defaults.dataset_view_url;
114
+ return template.replace("{server_url}", cleanServerUrl).replace("{id}", pkg.id).replace("{name}", pkg.name);
115
+ }
116
+ function getOrganizationViewUrl(serverUrl, org) {
117
+ const cleanServerUrl = normalizeUrl(serverUrl);
118
+ const portal = portals_default.portals.find((p) => {
119
+ const mainUrl = normalizeUrl(p.api_url);
120
+ const aliases = (p.api_url_aliases || []).map(normalizeUrl);
121
+ return mainUrl === cleanServerUrl || aliases.includes(cleanServerUrl);
122
+ });
123
+ const template = portal?.organization_view_url || portals_default.defaults.organization_view_url;
124
+ return template.replace("{server_url}", cleanServerUrl).replace("{id}", org.id).replace("{name}", org.name);
125
+ }
126
+
127
+ // src/tools/package.ts
128
+ function registerPackageTools(server2) {
129
+ server2.registerTool(
130
+ "ckan_package_search",
131
+ {
132
+ title: "Search CKAN Datasets",
133
+ description: `Search for datasets (packages) on a CKAN server using Solr query syntax.
134
+
135
+ Supports full Solr search capabilities including filters, facets, and sorting.
136
+ Use this to discover datasets matching specific criteria.
137
+
138
+ Args:
139
+ - server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it")
140
+ - q (string): Search query using Solr syntax (default: "*:*" for all)
141
+ - fq (string): Filter query (e.g., "organization:comune-palermo")
142
+ - rows (number): Number of results to return (default: 10, max: 1000)
143
+ - start (number): Offset for pagination (default: 0)
144
+ - sort (string): Sort field and direction (e.g., "metadata_modified desc")
145
+ - facet_field (array): Fields to facet on (e.g., ["organization", "tags"])
146
+ - facet_limit (number): Max facet values per field (default: 50)
147
+ - include_drafts (boolean): Include draft datasets (default: false)
148
+ - response_format ('markdown' | 'json'): Output format
149
+
150
+ Returns:
151
+ Search results with:
152
+ - count: Number of results found
153
+ - results: Array of dataset objects
154
+ - facets: Facet counts (if facet_field specified)
155
+ - search_facets: Detailed facet information
156
+
157
+ Query Syntax (parameter q):
158
+ Boolean operators:
159
+ - AND / &&: "water AND climate"
160
+ - OR / ||: "health OR sanit\xE0"
161
+ - NOT / !: "data NOT personal"
162
+ - +required -excluded: "+title:water -title:sea"
163
+ - Grouping: "(title:water OR title:climate) AND tags:environment"
164
+
165
+ Wildcards:
166
+ - *: "title:environment*" (matches environmental, environments, etc.)
167
+ - Note: Left truncation (*water) not supported
168
+
169
+ Fuzzy search (edit distance):
170
+ - ~: "title:rest~" or "title:rest~1" (finds "test", "best", "rest")
171
+
172
+ Proximity search (words within N positions):
173
+ - "phrase"~N: "title:"climate change"~5"
174
+
175
+ Range queries:
176
+ - Inclusive [a TO b]: "num_resources:[5 TO 10]"
177
+ - Exclusive {a TO b}: "num_resources:{0 TO 100}"
178
+ - One side open: "metadata_modified:[2024-01-01T00:00:00Z TO *]"
179
+
180
+ Date math:
181
+ - NOW-1YEAR, NOW-6MONTHS, NOW-7DAYS, NOW-1HOUR
182
+ - NOW/DAY, NOW/MONTH (round down)
183
+ - Combined: "metadata_modified:[NOW-2MONTHS TO NOW]"
184
+ - Example: "metadata_created:[NOW-1YEAR TO *]"
185
+
186
+ Field existence:
187
+ - Exists: "field:*" or "field:[* TO *]"
188
+ - Not exists: "NOT field:*" or "-field:*"
189
+
190
+ Boosting (relevance scoring):
191
+ - Boost term: "title:water^2 OR notes:water" (title matches score higher)
192
+ - Constant score: "title:water^=1.5"
193
+
194
+ Examples:
195
+ - Search all: { q: "*:*" }
196
+ - By tag: { q: "tags:sanit\xE0" }
197
+ - Boolean: { q: "(title:water OR title:climate) AND NOT title:sea" }
198
+ - Wildcard: { q: "title:environment*" }
199
+ - Fuzzy: { q: "title:health~2" }
200
+ - Proximity: { q: "notes:"open data"~3" }
201
+ - Date range: { q: "metadata_modified:[2024-01-01T00:00:00Z TO 2024-12-31T23:59:59Z]" }
202
+ - Date math: { q: "metadata_modified:[NOW-6MONTHS TO *]" }
203
+ - Field exists: { q: "organization:* AND num_resources:[1 TO *]" }
204
+ - Boosting: { q: "title:climate^2 OR notes:climate" }
205
+ - Filter org: { fq: "organization:regione-siciliana" }
206
+ - Get facets: { facet_field: ["organization"], rows: 0 }`,
207
+ inputSchema: z2.object({
208
+ server_url: z2.string().url("Must be a valid URL").describe("Base URL of the CKAN server"),
209
+ q: z2.string().optional().default("*:*").describe("Search query in Solr syntax"),
210
+ fq: z2.string().optional().describe("Filter query in Solr syntax"),
211
+ rows: z2.number().int().min(0).max(1e3).optional().default(10).describe("Number of results to return"),
212
+ start: z2.number().int().min(0).optional().default(0).describe("Offset for pagination"),
213
+ sort: z2.string().optional().describe("Sort field and direction (e.g., 'metadata_modified desc')"),
214
+ facet_field: z2.array(z2.string()).optional().describe("Fields to facet on"),
215
+ facet_limit: z2.number().int().min(1).optional().default(50).describe("Maximum facet values per field"),
216
+ include_drafts: z2.boolean().optional().default(false).describe("Include draft datasets"),
217
+ response_format: ResponseFormatSchema
218
+ }).strict(),
219
+ annotations: {
220
+ readOnlyHint: true,
221
+ destructiveHint: false,
222
+ idempotentHint: true,
223
+ openWorldHint: true
224
+ }
225
+ },
226
+ async (params) => {
227
+ try {
228
+ const apiParams = {
229
+ q: params.q,
230
+ rows: params.rows,
231
+ start: params.start,
232
+ include_private: params.include_drafts
233
+ };
234
+ if (params.fq) apiParams.fq = params.fq;
235
+ if (params.sort) apiParams.sort = params.sort;
236
+ if (params.facet_field && params.facet_field.length > 0) {
237
+ apiParams["facet.field"] = JSON.stringify(params.facet_field);
238
+ apiParams["facet.limit"] = params.facet_limit;
239
+ }
240
+ const result = await makeCkanRequest(
241
+ params.server_url,
242
+ "package_search",
243
+ apiParams
244
+ );
245
+ if (params.response_format === "json" /* JSON */) {
246
+ return {
247
+ content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
248
+ structuredContent: result
249
+ };
250
+ }
251
+ let markdown = `# CKAN Package Search Results
252
+
253
+ **Server**: ${params.server_url}
254
+ **Query**: ${params.q}
255
+ ${params.fq ? `**Filter**: ${params.fq}
256
+ ` : ""}
257
+ **Total Results**: ${result.count}
258
+ **Showing**: ${result.results.length} results (from ${params.start})
259
+
260
+ `;
261
+ if (result.facets && Object.keys(result.facets).length > 0) {
262
+ markdown += `## Facets
263
+
264
+ `;
265
+ for (const [field, values] of Object.entries(result.facets)) {
266
+ markdown += `### ${field}
267
+
268
+ `;
269
+ const facetValues = values;
270
+ const sorted = Object.entries(facetValues).sort((a, b) => b[1] - a[1]).slice(0, 10);
271
+ for (const [value, count] of sorted) {
272
+ markdown += `- **${value}**: ${count}
273
+ `;
274
+ }
275
+ markdown += "\n";
276
+ }
277
+ }
278
+ if (result.results && result.results.length > 0) {
279
+ markdown += `## Datasets
280
+
281
+ `;
282
+ for (const pkg of result.results) {
283
+ markdown += `### ${pkg.title || pkg.name}
284
+
285
+ `;
286
+ markdown += `- **ID**: \`${pkg.id}\`
287
+ `;
288
+ markdown += `- **Name**: \`${pkg.name}\`
289
+ `;
290
+ if (pkg.organization) {
291
+ markdown += `- **Organization**: ${pkg.organization.title || pkg.organization.name}
292
+ `;
293
+ }
294
+ if (pkg.notes) {
295
+ const notes = pkg.notes.substring(0, 200);
296
+ markdown += `- **Description**: ${notes}${pkg.notes.length > 200 ? "..." : ""}
297
+ `;
298
+ }
299
+ if (pkg.tags && pkg.tags.length > 0) {
300
+ const tags = pkg.tags.slice(0, 5).map((t) => t.name).join(", ");
301
+ markdown += `- **Tags**: ${tags}${pkg.tags.length > 5 ? ", ..." : ""}
302
+ `;
303
+ }
304
+ markdown += `- **Resources**: ${pkg.num_resources || 0}
305
+ `;
306
+ markdown += `- **Modified**: ${formatDate(pkg.metadata_modified)}
307
+ `;
308
+ markdown += `- **Link**: ${getDatasetViewUrl(params.server_url, pkg)}
309
+
310
+ `;
311
+ }
312
+ } else {
313
+ markdown += `No datasets found matching your query.
314
+ `;
315
+ }
316
+ if (result.count > params.start + params.rows) {
317
+ const nextStart = params.start + params.rows;
318
+ markdown += `
319
+ ---
320
+ **More results available**: Use \`start: ${nextStart}\` to see next page.
321
+ `;
322
+ }
323
+ return {
324
+ content: [{ type: "text", text: truncateText(markdown) }]
325
+ };
326
+ } catch (error) {
327
+ return {
328
+ content: [{
329
+ type: "text",
330
+ text: `Error searching packages: ${error instanceof Error ? error.message : String(error)}`
331
+ }],
332
+ isError: true
333
+ };
334
+ }
335
+ }
336
+ );
337
+ server2.registerTool(
338
+ "ckan_package_show",
339
+ {
340
+ title: "Show CKAN Dataset Details",
341
+ description: `Get complete metadata for a specific dataset (package).
342
+
343
+ Returns full details including resources, organization, tags, and all metadata fields.
344
+
345
+ Args:
346
+ - server_url (string): Base URL of CKAN server
347
+ - id (string): Dataset ID or name (machine-readable slug)
348
+ - include_tracking (boolean): Include view/download statistics (default: false)
349
+ - response_format ('markdown' | 'json'): Output format
350
+
351
+ Returns:
352
+ Complete dataset object with all metadata and resources
353
+
354
+ Examples:
355
+ - { server_url: "https://dati.gov.it", id: "dataset-name" }
356
+ - { server_url: "...", id: "abc-123-def", include_tracking: true }`,
357
+ inputSchema: z2.object({
358
+ server_url: z2.string().url().describe("Base URL of the CKAN server"),
359
+ id: z2.string().min(1).describe("Dataset ID or name"),
360
+ include_tracking: z2.boolean().optional().default(false).describe("Include tracking statistics"),
361
+ response_format: ResponseFormatSchema
362
+ }).strict(),
363
+ annotations: {
364
+ readOnlyHint: true,
365
+ destructiveHint: false,
366
+ idempotentHint: true,
367
+ openWorldHint: false
368
+ }
369
+ },
370
+ async (params) => {
371
+ try {
372
+ const result = await makeCkanRequest(
373
+ params.server_url,
374
+ "package_show",
375
+ {
376
+ id: params.id,
377
+ include_tracking: params.include_tracking
378
+ }
379
+ );
380
+ if (params.response_format === "json" /* JSON */) {
381
+ return {
382
+ content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
383
+ structuredContent: result
384
+ };
385
+ }
386
+ let markdown = `# Dataset: ${result.title || result.name}
387
+
388
+ `;
389
+ markdown += `**Server**: ${params.server_url}
390
+ `;
391
+ markdown += `**Link**: ${getDatasetViewUrl(params.server_url, result)}
392
+
393
+ `;
394
+ markdown += `## Basic Information
395
+
396
+ `;
397
+ markdown += `- **ID**: \`${result.id}\`
398
+ `;
399
+ markdown += `- **Name**: \`${result.name}\`
400
+ `;
401
+ if (result.author) markdown += `- **Author**: ${result.author}
402
+ `;
403
+ if (result.author_email) markdown += `- **Author Email**: ${result.author_email}
404
+ `;
405
+ if (result.maintainer) markdown += `- **Maintainer**: ${result.maintainer}
406
+ `;
407
+ if (result.maintainer_email) markdown += `- **Maintainer Email**: ${result.maintainer_email}
408
+ `;
409
+ markdown += `- **License**: ${result.license_title || result.license_id || "Not specified"}
410
+ `;
411
+ markdown += `- **State**: ${result.state}
412
+ `;
413
+ markdown += `- **Created**: ${formatDate(result.metadata_created)}
414
+ `;
415
+ markdown += `- **Modified**: ${formatDate(result.metadata_modified)}
416
+
417
+ `;
418
+ if (result.organization) {
419
+ markdown += `## Organization
420
+
421
+ `;
422
+ markdown += `- **Name**: ${result.organization.title || result.organization.name}
423
+ `;
424
+ markdown += `- **ID**: \`${result.organization.id}\`
425
+
426
+ `;
427
+ }
428
+ if (result.notes) {
429
+ markdown += `## Description
430
+
431
+ ${result.notes}
432
+
433
+ `;
434
+ }
435
+ if (result.tags && result.tags.length > 0) {
436
+ markdown += `## Tags
437
+
438
+ `;
439
+ markdown += result.tags.map((t) => `- ${t.name}`).join("\n") + "\n\n";
440
+ }
441
+ if (result.groups && result.groups.length > 0) {
442
+ markdown += `## Groups
443
+
444
+ `;
445
+ for (const group of result.groups) {
446
+ markdown += `- **${group.title || group.name}** (\`${group.name}\`)
447
+ `;
448
+ }
449
+ markdown += "\n";
450
+ }
451
+ if (result.resources && result.resources.length > 0) {
452
+ markdown += `## Resources (${result.resources.length})
453
+
454
+ `;
455
+ for (const resource of result.resources) {
456
+ markdown += `### ${resource.name || "Unnamed Resource"}
457
+
458
+ `;
459
+ markdown += `- **ID**: \`${resource.id}\`
460
+ `;
461
+ markdown += `- **Format**: ${resource.format || "Unknown"}
462
+ `;
463
+ if (resource.description) markdown += `- **Description**: ${resource.description}
464
+ `;
465
+ markdown += `- **URL**: ${resource.url}
466
+ `;
467
+ if (resource.size) {
468
+ const formatBytes = (bytes) => {
469
+ if (!bytes || bytes === 0) return "0 B";
470
+ const k = 1024;
471
+ const sizes = ["B", "KB", "MB", "GB"];
472
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
473
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
474
+ };
475
+ markdown += `- **Size**: ${formatBytes(resource.size)}
476
+ `;
477
+ }
478
+ if (resource.mimetype) markdown += `- **MIME Type**: ${resource.mimetype}
479
+ `;
480
+ markdown += `- **Created**: ${formatDate(resource.created)}
481
+ `;
482
+ if (resource.last_modified) markdown += `- **Modified**: ${formatDate(resource.last_modified)}
483
+ `;
484
+ if (resource.datastore_active !== void 0) {
485
+ markdown += `- **DataStore**: ${resource.datastore_active ? "\u2705 Available" : "\u274C Not available"}
486
+ `;
487
+ }
488
+ markdown += "\n";
489
+ }
490
+ }
491
+ if (result.extras && result.extras.length > 0) {
492
+ markdown += `## Extra Fields
493
+
494
+ `;
495
+ for (const extra of result.extras) {
496
+ markdown += `- **${extra.key}**: ${extra.value}
497
+ `;
498
+ }
499
+ markdown += "\n";
500
+ }
501
+ return {
502
+ content: [{ type: "text", text: truncateText(markdown) }]
503
+ };
504
+ } catch (error) {
505
+ return {
506
+ content: [{
507
+ type: "text",
508
+ text: `Error fetching package: ${error instanceof Error ? error.message : String(error)}`
509
+ }],
510
+ isError: true
511
+ };
512
+ }
513
+ }
514
+ );
515
+ }
516
+
517
+ // src/tools/organization.ts
518
+ import { z as z3 } from "zod";
519
+ function registerOrganizationTools(server2) {
520
+ server2.registerTool(
521
+ "ckan_organization_list",
522
+ {
523
+ title: "List CKAN Organizations",
524
+ description: `List all organizations on a CKAN server.
525
+
526
+ Organizations are entities that publish and manage datasets.
527
+
528
+ Args:
529
+ - server_url (string): Base URL of CKAN server
530
+ - all_fields (boolean): Return full objects vs just names (default: false)
531
+ - sort (string): Sort field (default: "name asc")
532
+ - limit (number): Maximum results (default: 100). Use 0 to get only the count via faceting
533
+ - offset (number): Pagination offset (default: 0)
534
+ - response_format ('markdown' | 'json'): Output format
535
+
536
+ Returns:
537
+ List of organizations with metadata. When limit=0, returns only the count of organizations with datasets.`,
538
+ inputSchema: z3.object({
539
+ server_url: z3.string().url(),
540
+ all_fields: z3.boolean().optional().default(false),
541
+ sort: z3.string().optional().default("name asc"),
542
+ limit: z3.number().int().min(0).optional().default(100),
543
+ offset: z3.number().int().min(0).optional().default(0),
544
+ response_format: ResponseFormatSchema
545
+ }).strict(),
546
+ annotations: {
547
+ readOnlyHint: true,
548
+ destructiveHint: false,
549
+ idempotentHint: true,
550
+ openWorldHint: false
551
+ }
552
+ },
553
+ async (params) => {
554
+ try {
555
+ if (params.limit === 0) {
556
+ const searchResult = await makeCkanRequest(
557
+ params.server_url,
558
+ "package_search",
559
+ {
560
+ rows: 0,
561
+ "facet.field": JSON.stringify(["organization"]),
562
+ "facet.limit": -1
563
+ }
564
+ );
565
+ const orgCount = searchResult.search_facets?.organization?.items?.length || 0;
566
+ if (params.response_format === "json" /* JSON */) {
567
+ return {
568
+ content: [{ type: "text", text: JSON.stringify({ count: orgCount }, null, 2) }],
569
+ structuredContent: { count: orgCount }
570
+ };
571
+ }
572
+ const markdown2 = `# CKAN Organizations Count
573
+
574
+ **Server**: ${params.server_url}
575
+ **Total organizations (with datasets)**: ${orgCount}
576
+ `;
577
+ return {
578
+ content: [{ type: "text", text: markdown2 }]
579
+ };
580
+ }
581
+ const result = await makeCkanRequest(
582
+ params.server_url,
583
+ "organization_list",
584
+ {
585
+ all_fields: params.all_fields,
586
+ sort: params.sort,
587
+ limit: params.limit,
588
+ offset: params.offset
589
+ }
590
+ );
591
+ if (params.response_format === "json" /* JSON */) {
592
+ const output = Array.isArray(result) ? { count: result.length, organizations: result } : result;
593
+ return {
594
+ content: [{ type: "text", text: truncateText(JSON.stringify(output, null, 2)) }],
595
+ structuredContent: output
596
+ };
597
+ }
598
+ let markdown = `# CKAN Organizations
599
+
600
+ `;
601
+ markdown += `**Server**: ${params.server_url}
602
+ `;
603
+ markdown += `**Total**: ${Array.isArray(result) ? result.length : "Unknown"}
604
+
605
+ `;
606
+ if (Array.isArray(result)) {
607
+ if (params.all_fields) {
608
+ for (const org of result) {
609
+ markdown += `## ${org.title || org.name}
610
+
611
+ `;
612
+ markdown += `- **ID**: \`${org.id}\`
613
+ `;
614
+ markdown += `- **Name**: \`${org.name}\`
615
+ `;
616
+ if (org.description) markdown += `- **Description**: ${org.description.substring(0, 200)}
617
+ `;
618
+ markdown += `- **Datasets**: ${org.package_count || 0}
619
+ `;
620
+ markdown += `- **Created**: ${formatDate(org.created)}
621
+ `;
622
+ markdown += `- **Link**: ${getOrganizationViewUrl(params.server_url, org)}
623
+
624
+ `;
625
+ }
626
+ } else {
627
+ markdown += result.map((name) => `- ${name}`).join("\n");
628
+ }
629
+ }
630
+ return {
631
+ content: [{ type: "text", text: truncateText(markdown) }]
632
+ };
633
+ } catch (error) {
634
+ return {
635
+ content: [{
636
+ type: "text",
637
+ text: `Error listing organizations: ${error instanceof Error ? error.message : String(error)}`
638
+ }],
639
+ isError: true
640
+ };
641
+ }
642
+ }
643
+ );
644
+ server2.registerTool(
645
+ "ckan_organization_show",
646
+ {
647
+ title: "Show CKAN Organization Details",
648
+ description: `Get details of a specific organization.
649
+
650
+ Args:
651
+ - server_url (string): Base URL of CKAN server
652
+ - id (string): Organization ID or name
653
+ - include_datasets (boolean): Include list of datasets (default: true)
654
+ - include_users (boolean): Include list of users (default: false)
655
+ - response_format ('markdown' | 'json'): Output format
656
+
657
+ Returns:
658
+ Organization details with optional datasets and users`,
659
+ inputSchema: z3.object({
660
+ server_url: z3.string().url(),
661
+ id: z3.string().min(1),
662
+ include_datasets: z3.boolean().optional().default(true),
663
+ include_users: z3.boolean().optional().default(false),
664
+ response_format: ResponseFormatSchema
665
+ }).strict(),
666
+ annotations: {
667
+ readOnlyHint: true,
668
+ destructiveHint: false,
669
+ idempotentHint: true,
670
+ openWorldHint: false
671
+ }
672
+ },
673
+ async (params) => {
674
+ try {
675
+ const result = await makeCkanRequest(
676
+ params.server_url,
677
+ "organization_show",
678
+ {
679
+ id: params.id,
680
+ include_datasets: params.include_datasets,
681
+ include_users: params.include_users
682
+ }
683
+ );
684
+ if (params.response_format === "json" /* JSON */) {
685
+ return {
686
+ content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
687
+ structuredContent: result
688
+ };
689
+ }
690
+ let markdown = `# Organization: ${result.title || result.name}
691
+
692
+ `;
693
+ markdown += `**Server**: ${params.server_url}
694
+ `;
695
+ markdown += `**Link**: ${getOrganizationViewUrl(params.server_url, result)}
696
+
697
+ `;
698
+ markdown += `## Details
699
+
700
+ `;
701
+ markdown += `- **ID**: \`${result.id}\`
702
+ `;
703
+ markdown += `- **Name**: \`${result.name}\`
704
+ `;
705
+ markdown += `- **Datasets**: ${result.package_count || 0}
706
+ `;
707
+ markdown += `- **Created**: ${formatDate(result.created)}
708
+ `;
709
+ markdown += `- **State**: ${result.state}
710
+
711
+ `;
712
+ if (result.description) {
713
+ markdown += `## Description
714
+
715
+ ${result.description}
716
+
717
+ `;
718
+ }
719
+ if (result.packages && result.packages.length > 0) {
720
+ markdown += `## Datasets (${result.packages.length})
721
+
722
+ `;
723
+ for (const pkg of result.packages.slice(0, 20)) {
724
+ markdown += `- **${pkg.title || pkg.name}** (\`${pkg.name}\`)
725
+ `;
726
+ }
727
+ if (result.packages.length > 20) {
728
+ markdown += `
729
+ ... and ${result.packages.length - 20} more datasets
730
+ `;
731
+ }
732
+ markdown += "\n";
733
+ }
734
+ if (result.users && result.users.length > 0) {
735
+ markdown += `## Users (${result.users.length})
736
+
737
+ `;
738
+ for (const user of result.users) {
739
+ markdown += `- **${user.name}** (${user.capacity})
740
+ `;
741
+ }
742
+ markdown += "\n";
743
+ }
744
+ return {
745
+ content: [{ type: "text", text: truncateText(markdown) }]
746
+ };
747
+ } catch (error) {
748
+ return {
749
+ content: [{
750
+ type: "text",
751
+ text: `Error fetching organization: ${error instanceof Error ? error.message : String(error)}`
752
+ }],
753
+ isError: true
754
+ };
755
+ }
756
+ }
757
+ );
758
+ server2.registerTool(
759
+ "ckan_organization_search",
760
+ {
761
+ title: "Search CKAN Organizations by Name",
762
+ description: `Search for organizations by name pattern.
763
+
764
+ This tool provides a simpler interface than package_search for finding organizations.
765
+ Wildcards are automatically added around the search pattern.
766
+
767
+ Args:
768
+ - server_url (string): Base URL of CKAN server
769
+ - pattern (string): Search pattern (e.g., "toscana", "salute")
770
+ - response_format ('markdown' | 'json'): Output format
771
+
772
+ Returns:
773
+ List of matching organizations with dataset counts
774
+
775
+ Examples:
776
+ - { server_url: "https://www.dati.gov.it/opendata", pattern: "toscana" }
777
+ - { server_url: "https://catalog.data.gov", pattern: "health" }`,
778
+ inputSchema: z3.object({
779
+ server_url: z3.string().url(),
780
+ pattern: z3.string().min(1).describe("Search pattern (wildcards added automatically)"),
781
+ response_format: ResponseFormatSchema
782
+ }).strict(),
783
+ annotations: {
784
+ readOnlyHint: true,
785
+ destructiveHint: false,
786
+ idempotentHint: true,
787
+ openWorldHint: true
788
+ }
789
+ },
790
+ async (params) => {
791
+ try {
792
+ const query = `organization:*${params.pattern}*`;
793
+ const result = await makeCkanRequest(
794
+ params.server_url,
795
+ "package_search",
796
+ {
797
+ q: query,
798
+ rows: 0,
799
+ "facet.field": JSON.stringify(["organization"]),
800
+ "facet.limit": 500
801
+ }
802
+ );
803
+ const orgFacets = result.search_facets?.organization?.items || [];
804
+ const totalDatasets = result.count || 0;
805
+ if (params.response_format === "json" /* JSON */) {
806
+ const jsonResult = {
807
+ count: orgFacets.length,
808
+ total_datasets: totalDatasets,
809
+ organizations: orgFacets.map((item) => ({
810
+ name: item.name,
811
+ display_name: item.display_name,
812
+ dataset_count: item.count
813
+ }))
814
+ };
815
+ return {
816
+ content: [{ type: "text", text: truncateText(JSON.stringify(jsonResult, null, 2)) }],
817
+ structuredContent: jsonResult
818
+ };
819
+ }
820
+ let markdown = `# CKAN Organization Search Results
821
+
822
+ `;
823
+ markdown += `**Server**: ${params.server_url}
824
+ `;
825
+ markdown += `**Pattern**: "${params.pattern}"
826
+ `;
827
+ markdown += `**Organizations Found**: ${orgFacets.length}
828
+ `;
829
+ markdown += `**Total Datasets**: ${totalDatasets}
830
+
831
+ `;
832
+ if (orgFacets.length === 0) {
833
+ markdown += `No organizations found matching pattern "${params.pattern}".
834
+ `;
835
+ } else {
836
+ markdown += `## Matching Organizations
837
+
838
+ `;
839
+ markdown += `| Organization | Datasets |
840
+ `;
841
+ markdown += `|--------------|----------|
842
+ `;
843
+ for (const org of orgFacets) {
844
+ markdown += `| ${org.display_name || org.name} | ${org.count} |
845
+ `;
846
+ }
847
+ }
848
+ return {
849
+ content: [{ type: "text", text: truncateText(markdown) }]
850
+ };
851
+ } catch (error) {
852
+ return {
853
+ content: [{
854
+ type: "text",
855
+ text: `Error searching organizations: ${error instanceof Error ? error.message : String(error)}`
856
+ }],
857
+ isError: true
858
+ };
859
+ }
860
+ }
861
+ );
862
+ }
863
+
864
+ // src/tools/datastore.ts
865
+ import { z as z4 } from "zod";
866
+ function registerDatastoreTools(server2) {
867
+ server2.registerTool(
868
+ "ckan_datastore_search",
869
+ {
870
+ title: "Search CKAN DataStore",
871
+ description: `Query data from a CKAN DataStore resource.
872
+
873
+ The DataStore allows SQL-like queries on tabular data. Not all resources have DataStore enabled.
874
+
875
+ Args:
876
+ - server_url (string): Base URL of CKAN server
877
+ - resource_id (string): ID of the DataStore resource
878
+ - q (string): Full-text search query (optional)
879
+ - filters (object): Key-value filters (e.g., { "anno": 2023 })
880
+ - limit (number): Max rows to return (default: 100, max: 32000)
881
+ - offset (number): Pagination offset (default: 0)
882
+ - fields (array): Specific fields to return (optional)
883
+ - sort (string): Sort field with direction (e.g., "anno desc")
884
+ - distinct (boolean): Return distinct values (default: false)
885
+ - response_format ('markdown' | 'json'): Output format
886
+
887
+ Returns:
888
+ DataStore records matching query
889
+
890
+ Examples:
891
+ - { server_url: "...", resource_id: "abc-123", limit: 50 }
892
+ - { server_url: "...", resource_id: "...", filters: { "regione": "Sicilia" } }
893
+ - { server_url: "...", resource_id: "...", sort: "anno desc", limit: 100 }`,
894
+ inputSchema: z4.object({
895
+ server_url: z4.string().url(),
896
+ resource_id: z4.string().min(1),
897
+ q: z4.string().optional(),
898
+ filters: z4.record(z4.any()).optional(),
899
+ limit: z4.number().int().min(1).max(32e3).optional().default(100),
900
+ offset: z4.number().int().min(0).optional().default(0),
901
+ fields: z4.array(z4.string()).optional(),
902
+ sort: z4.string().optional(),
903
+ distinct: z4.boolean().optional().default(false),
904
+ response_format: ResponseFormatSchema
905
+ }).strict(),
906
+ annotations: {
907
+ readOnlyHint: true,
908
+ destructiveHint: false,
909
+ idempotentHint: true,
910
+ openWorldHint: false
911
+ }
912
+ },
913
+ async (params) => {
914
+ try {
915
+ const apiParams = {
916
+ resource_id: params.resource_id,
917
+ limit: params.limit,
918
+ offset: params.offset,
919
+ distinct: params.distinct
920
+ };
921
+ if (params.q) apiParams.q = params.q;
922
+ if (params.filters) apiParams.filters = JSON.stringify(params.filters);
923
+ if (params.fields) apiParams.fields = params.fields.join(",");
924
+ if (params.sort) apiParams.sort = params.sort;
925
+ const result = await makeCkanRequest(
926
+ params.server_url,
927
+ "datastore_search",
928
+ apiParams
929
+ );
930
+ if (params.response_format === "json" /* JSON */) {
931
+ return {
932
+ content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
933
+ structuredContent: result
934
+ };
935
+ }
936
+ let markdown = `# DataStore Query Results
937
+
938
+ `;
939
+ markdown += `**Server**: ${params.server_url}
940
+ `;
941
+ markdown += `**Resource ID**: \`${params.resource_id}\`
942
+ `;
943
+ markdown += `**Total Records**: ${result.total || 0}
944
+ `;
945
+ markdown += `**Returned**: ${result.records ? result.records.length : 0} records
946
+
947
+ `;
948
+ if (result.fields && result.fields.length > 0) {
949
+ markdown += `## Fields
950
+
951
+ `;
952
+ markdown += result.fields.map((f) => `- **${f.id}** (${f.type})`).join("\n") + "\n\n";
953
+ }
954
+ if (result.records && result.records.length > 0) {
955
+ markdown += `## Records
956
+
957
+ `;
958
+ const fields = result.fields.map((f) => f.id);
959
+ const displayFields = fields.slice(0, 8);
960
+ markdown += `| ${displayFields.join(" | ")} |
961
+ `;
962
+ markdown += `| ${displayFields.map(() => "---").join(" | ")} |
963
+ `;
964
+ for (const record of result.records.slice(0, 50)) {
965
+ const values = displayFields.map((field) => {
966
+ const val = record[field];
967
+ if (val === null || val === void 0) return "-";
968
+ const str = String(val);
969
+ return str.length > 50 ? str.substring(0, 47) + "..." : str;
970
+ });
971
+ markdown += `| ${values.join(" | ")} |
972
+ `;
973
+ }
974
+ if (result.records.length > 50) {
975
+ markdown += `
976
+ ... and ${result.records.length - 50} more records
977
+ `;
978
+ }
979
+ markdown += "\n";
980
+ }
981
+ if (result.total && result.total > params.offset + (result.records?.length || 0)) {
982
+ const nextOffset = params.offset + params.limit;
983
+ markdown += `**More results available**: Use \`offset: ${nextOffset}\` for next page.
984
+ `;
985
+ }
986
+ return {
987
+ content: [{ type: "text", text: truncateText(markdown) }]
988
+ };
989
+ } catch (error) {
990
+ return {
991
+ content: [{
992
+ type: "text",
993
+ text: `Error querying DataStore: ${error instanceof Error ? error.message : String(error)}`
994
+ }],
995
+ isError: true
996
+ };
997
+ }
998
+ }
999
+ );
1000
+ }
1001
+
1002
+ // src/tools/status.ts
1003
+ import { z as z5 } from "zod";
1004
+ function registerStatusTools(server2) {
1005
+ server2.registerTool(
1006
+ "ckan_status_show",
1007
+ {
1008
+ title: "Check CKAN Server Status",
1009
+ description: `Check if a CKAN server is available and get version information.
1010
+
1011
+ Useful to verify server accessibility before making other requests.
1012
+
1013
+ Args:
1014
+ - server_url (string): Base URL of CKAN server
1015
+
1016
+ Returns:
1017
+ Server status and version information`,
1018
+ inputSchema: z5.object({
1019
+ server_url: z5.string().url().describe("Base URL of the CKAN server")
1020
+ }).strict(),
1021
+ annotations: {
1022
+ readOnlyHint: true,
1023
+ destructiveHint: false,
1024
+ idempotentHint: true,
1025
+ openWorldHint: false
1026
+ }
1027
+ },
1028
+ async (params) => {
1029
+ try {
1030
+ const result = await makeCkanRequest(
1031
+ params.server_url,
1032
+ "status_show",
1033
+ {}
1034
+ );
1035
+ const markdown = `# CKAN Server Status
1036
+
1037
+ **Server**: ${params.server_url}
1038
+ **Status**: \u2705 Online
1039
+ **CKAN Version**: ${result.ckan_version || "Unknown"}
1040
+ **Site Title**: ${result.site_title || "N/A"}
1041
+ **Site URL**: ${result.site_url || "N/A"}
1042
+ `;
1043
+ return {
1044
+ content: [{ type: "text", text: markdown }],
1045
+ structuredContent: result
1046
+ };
1047
+ } catch (error) {
1048
+ return {
1049
+ content: [{
1050
+ type: "text",
1051
+ text: `Server appears to be offline or not a valid CKAN instance:
1052
+ ${error instanceof Error ? error.message : String(error)}`
1053
+ }],
1054
+ isError: true
1055
+ };
1056
+ }
1057
+ }
1058
+ );
1059
+ }
1060
+
1061
+ // src/resources/dataset.ts
1062
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1063
+
1064
+ // src/resources/uri.ts
1065
+ function parseCkanUri(uri) {
1066
+ const hostname = uri.hostname;
1067
+ if (!hostname) {
1068
+ throw new Error("Invalid ckan:// URI: missing server hostname");
1069
+ }
1070
+ const pathParts = uri.pathname.split("/").filter((p) => p.length > 0);
1071
+ if (pathParts.length < 2) {
1072
+ throw new Error(
1073
+ `Invalid ckan:// URI: expected /{type}/{id}, got ${uri.pathname}`
1074
+ );
1075
+ }
1076
+ const [type, ...idParts] = pathParts;
1077
+ const id = idParts.join("/");
1078
+ if (!type || !id) {
1079
+ throw new Error("Invalid ckan:// URI: missing type or id");
1080
+ }
1081
+ const server2 = `https://${hostname}`;
1082
+ return { server: server2, type, id };
1083
+ }
1084
+
1085
+ // src/resources/dataset.ts
1086
+ function registerDatasetResource(server2) {
1087
+ server2.registerResource(
1088
+ "ckan-dataset",
1089
+ new ResourceTemplate("ckan://{server}/dataset/{id}", { list: void 0 }),
1090
+ {
1091
+ title: "CKAN Dataset",
1092
+ description: "Access dataset metadata from any CKAN server. URI format: ckan://{server}/dataset/{id}",
1093
+ mimeType: "application/json"
1094
+ },
1095
+ async (uri, variables) => {
1096
+ try {
1097
+ const { server: serverUrl } = parseCkanUri(uri);
1098
+ const id = variables.id;
1099
+ const result = await makeCkanRequest(serverUrl, "package_show", {
1100
+ id
1101
+ });
1102
+ const content = truncateText(JSON.stringify(result, null, 2));
1103
+ return {
1104
+ contents: [
1105
+ {
1106
+ uri: uri.href,
1107
+ mimeType: "application/json",
1108
+ text: content
1109
+ }
1110
+ ]
1111
+ };
1112
+ } catch (error) {
1113
+ const errorMessage = error instanceof Error ? error.message : String(error);
1114
+ return {
1115
+ contents: [
1116
+ {
1117
+ uri: uri.href,
1118
+ mimeType: "text/plain",
1119
+ text: `Error fetching dataset: ${errorMessage}`
1120
+ }
1121
+ ]
1122
+ };
1123
+ }
1124
+ }
1125
+ );
1126
+ }
1127
+
1128
+ // src/resources/resource.ts
1129
+ import { ResourceTemplate as ResourceTemplate2 } from "@modelcontextprotocol/sdk/server/mcp.js";
1130
+ function registerResourceResource(server2) {
1131
+ server2.registerResource(
1132
+ "ckan-resource",
1133
+ new ResourceTemplate2("ckan://{server}/resource/{id}", { list: void 0 }),
1134
+ {
1135
+ title: "CKAN Resource",
1136
+ description: "Access resource metadata and download URL from any CKAN server. URI format: ckan://{server}/resource/{id}",
1137
+ mimeType: "application/json"
1138
+ },
1139
+ async (uri, variables) => {
1140
+ try {
1141
+ const { server: serverUrl } = parseCkanUri(uri);
1142
+ const id = variables.id;
1143
+ const result = await makeCkanRequest(serverUrl, "resource_show", {
1144
+ id
1145
+ });
1146
+ const content = truncateText(JSON.stringify(result, null, 2));
1147
+ return {
1148
+ contents: [
1149
+ {
1150
+ uri: uri.href,
1151
+ mimeType: "application/json",
1152
+ text: content
1153
+ }
1154
+ ]
1155
+ };
1156
+ } catch (error) {
1157
+ const errorMessage = error instanceof Error ? error.message : String(error);
1158
+ return {
1159
+ contents: [
1160
+ {
1161
+ uri: uri.href,
1162
+ mimeType: "text/plain",
1163
+ text: `Error fetching resource: ${errorMessage}`
1164
+ }
1165
+ ]
1166
+ };
1167
+ }
1168
+ }
1169
+ );
1170
+ }
1171
+
1172
+ // src/resources/organization.ts
1173
+ import { ResourceTemplate as ResourceTemplate3 } from "@modelcontextprotocol/sdk/server/mcp.js";
1174
+ function registerOrganizationResource(server2) {
1175
+ server2.registerResource(
1176
+ "ckan-organization",
1177
+ new ResourceTemplate3("ckan://{server}/organization/{name}", {
1178
+ list: void 0
1179
+ }),
1180
+ {
1181
+ title: "CKAN Organization",
1182
+ description: "Access organization metadata from any CKAN server. URI format: ckan://{server}/organization/{name}",
1183
+ mimeType: "application/json"
1184
+ },
1185
+ async (uri, variables) => {
1186
+ try {
1187
+ const { server: serverUrl } = parseCkanUri(uri);
1188
+ const name = variables.name;
1189
+ const result = await makeCkanRequest(
1190
+ serverUrl,
1191
+ "organization_show",
1192
+ {
1193
+ id: name,
1194
+ include_datasets: false
1195
+ }
1196
+ );
1197
+ const content = truncateText(JSON.stringify(result, null, 2));
1198
+ return {
1199
+ contents: [
1200
+ {
1201
+ uri: uri.href,
1202
+ mimeType: "application/json",
1203
+ text: content
1204
+ }
1205
+ ]
1206
+ };
1207
+ } catch (error) {
1208
+ const errorMessage = error instanceof Error ? error.message : String(error);
1209
+ return {
1210
+ contents: [
1211
+ {
1212
+ uri: uri.href,
1213
+ mimeType: "text/plain",
1214
+ text: `Error fetching organization: ${errorMessage}`
1215
+ }
1216
+ ]
1217
+ };
1218
+ }
1219
+ }
1220
+ );
1221
+ }
1222
+
1223
+ // src/resources/index.ts
1224
+ function registerAllResources(server2) {
1225
+ registerDatasetResource(server2);
1226
+ registerResourceResource(server2);
1227
+ registerOrganizationResource(server2);
1228
+ }
1229
+
1230
+ // src/transport/stdio.ts
1231
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1232
+ async function runStdio(server2) {
1233
+ const transport2 = new StdioServerTransport();
1234
+ await server2.connect(transport2);
1235
+ console.error("CKAN MCP server running on stdio");
1236
+ }
1237
+
1238
+ // src/transport/http.ts
1239
+ import express from "express";
1240
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1241
+ async function runHTTP(server2) {
1242
+ const app = express();
1243
+ app.use(express.json());
1244
+ app.post("/mcp", async (req, res) => {
1245
+ const transport2 = new StreamableHTTPServerTransport({
1246
+ sessionIdGenerator: void 0,
1247
+ enableJsonResponse: true
1248
+ });
1249
+ res.on("close", () => transport2.close());
1250
+ await server2.connect(transport2);
1251
+ await transport2.handleRequest(req, res, req.body);
1252
+ });
1253
+ const port = parseInt(process.env.PORT || "3000");
1254
+ app.listen(port, () => {
1255
+ console.error(`CKAN MCP server running on http://localhost:${port}/mcp`);
1256
+ });
1257
+ }
1258
+
1259
+ // src/index.ts
1260
+ var server = createServer();
1261
+ registerPackageTools(server);
1262
+ registerOrganizationTools(server);
1263
+ registerDatastoreTools(server);
1264
+ registerStatusTools(server);
1265
+ registerAllResources(server);
1266
+ var transport = process.env.TRANSPORT || "stdio";
1267
+ if (transport === "http") {
1268
+ runHTTP(server).catch((error) => {
1269
+ console.error("Server error:", error);
1270
+ process.exit(1);
1271
+ });
1272
+ } else {
1273
+ runStdio(server).catch((error) => {
1274
+ console.error("Server error:", error);
1275
+ process.exit(1);
1276
+ });
1277
+ }