@aborruso/ckan-mcp-server 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1059,6 +1059,501 @@ ${error instanceof Error ? error.message : String(error)}`
1059
1059
  );
1060
1060
  }
1061
1061
 
1062
+ // src/tools/tag.ts
1063
+ import { z as z6 } from "zod";
1064
+ function normalizeTagFacets(result) {
1065
+ const searchItems = result?.search_facets?.tags?.items;
1066
+ if (Array.isArray(searchItems)) {
1067
+ return searchItems.map((item) => ({
1068
+ name: item?.name || item?.display_name || String(item),
1069
+ count: typeof item?.count === "number" ? item.count : 0,
1070
+ display_name: item?.display_name
1071
+ }));
1072
+ }
1073
+ const facets = result?.facets?.tags;
1074
+ if (Array.isArray(facets)) {
1075
+ if (facets.length > 0 && typeof facets[0] === "object") {
1076
+ return facets.map((item) => ({
1077
+ name: item?.name || item?.display_name || String(item),
1078
+ count: typeof item?.count === "number" ? item.count : 0,
1079
+ display_name: item?.display_name
1080
+ }));
1081
+ }
1082
+ return facets.map((name) => ({ name, count: 0 }));
1083
+ }
1084
+ if (facets && typeof facets === "object") {
1085
+ return Object.entries(facets).map(([name, count]) => ({
1086
+ name,
1087
+ count: typeof count === "number" ? count : Number(count) || 0
1088
+ }));
1089
+ }
1090
+ return [];
1091
+ }
1092
+ function registerTagTools(server2) {
1093
+ server2.registerTool(
1094
+ "ckan_tag_list",
1095
+ {
1096
+ title: "List CKAN Tags",
1097
+ description: `List tags from a CKAN server using faceting.
1098
+
1099
+ This returns tag names with counts, optionally filtered by dataset query or tag substring.
1100
+
1101
+ Args:
1102
+ - server_url (string): Base URL of CKAN server
1103
+ - q (string): Dataset search query (default: "*:*")
1104
+ - fq (string): Filter query (optional)
1105
+ - tag_query (string): Filter tags by substring (optional)
1106
+ - limit (number): Max tags to return (default: 100, max: 1000)
1107
+ - response_format ('markdown' | 'json'): Output format
1108
+
1109
+ Returns:
1110
+ List of tags with counts (from faceting)`,
1111
+ inputSchema: z6.object({
1112
+ server_url: z6.string().url(),
1113
+ q: z6.string().optional().default("*:*"),
1114
+ fq: z6.string().optional(),
1115
+ tag_query: z6.string().optional(),
1116
+ limit: z6.number().int().min(1).max(1e3).optional().default(100),
1117
+ response_format: ResponseFormatSchema
1118
+ }).strict(),
1119
+ annotations: {
1120
+ readOnlyHint: true,
1121
+ destructiveHint: false,
1122
+ idempotentHint: true,
1123
+ openWorldHint: true
1124
+ }
1125
+ },
1126
+ async (params) => {
1127
+ try {
1128
+ const apiParams = {
1129
+ q: params.q,
1130
+ rows: 0,
1131
+ "facet.field": JSON.stringify(["tags"]),
1132
+ "facet.limit": params.limit
1133
+ };
1134
+ if (params.fq) apiParams.fq = params.fq;
1135
+ const result = await makeCkanRequest(
1136
+ params.server_url,
1137
+ "package_search",
1138
+ apiParams
1139
+ );
1140
+ let tags = normalizeTagFacets(result);
1141
+ if (params.tag_query) {
1142
+ const needle = params.tag_query.toLowerCase();
1143
+ tags = tags.filter((tag) => tag.name.toLowerCase().includes(needle));
1144
+ }
1145
+ tags = tags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)).slice(0, params.limit);
1146
+ if (params.response_format === "json" /* JSON */) {
1147
+ const output = {
1148
+ count: tags.length,
1149
+ tags
1150
+ };
1151
+ return {
1152
+ content: [{ type: "text", text: truncateText(JSON.stringify(output, null, 2)) }],
1153
+ structuredContent: output
1154
+ };
1155
+ }
1156
+ let markdown = `# CKAN Tags
1157
+
1158
+ `;
1159
+ markdown += `**Server**: ${params.server_url}
1160
+ `;
1161
+ markdown += `**Query**: ${params.q}
1162
+ `;
1163
+ if (params.fq) markdown += `**Filter**: ${params.fq}
1164
+ `;
1165
+ if (params.tag_query) markdown += `**Tag Query**: ${params.tag_query}
1166
+ `;
1167
+ markdown += `**Count**: ${tags.length}
1168
+
1169
+ `;
1170
+ if (tags.length === 0) {
1171
+ markdown += `No tags found.
1172
+ `;
1173
+ } else {
1174
+ for (const tag of tags) {
1175
+ markdown += `- **${tag.name}**: ${tag.count}
1176
+ `;
1177
+ }
1178
+ }
1179
+ return {
1180
+ content: [{ type: "text", text: truncateText(markdown) }]
1181
+ };
1182
+ } catch (error) {
1183
+ return {
1184
+ content: [{
1185
+ type: "text",
1186
+ text: `Error listing tags: ${error instanceof Error ? error.message : String(error)}`
1187
+ }],
1188
+ isError: true
1189
+ };
1190
+ }
1191
+ }
1192
+ );
1193
+ }
1194
+
1195
+ // src/tools/group.ts
1196
+ import { z as z7 } from "zod";
1197
+ function getGroupViewUrl(serverUrl, group) {
1198
+ const cleanServerUrl = serverUrl.replace(/\/$/, "");
1199
+ return `${cleanServerUrl}/group/${group.name}`;
1200
+ }
1201
+ function normalizeGroupFacets(result) {
1202
+ const items = result?.search_facets?.groups?.items;
1203
+ if (Array.isArray(items)) {
1204
+ return items.map((item) => ({
1205
+ name: item?.name || item?.display_name || String(item),
1206
+ display_name: item?.display_name,
1207
+ count: typeof item?.count === "number" ? item.count : 0
1208
+ }));
1209
+ }
1210
+ const facets = result?.facets?.groups;
1211
+ if (Array.isArray(facets)) {
1212
+ if (facets.length > 0 && typeof facets[0] === "object") {
1213
+ return facets.map((item) => ({
1214
+ name: item?.name || item?.display_name || String(item),
1215
+ display_name: item?.display_name,
1216
+ count: typeof item?.count === "number" ? item.count : 0
1217
+ }));
1218
+ }
1219
+ return facets.map((name) => ({ name, count: 0 }));
1220
+ }
1221
+ if (facets && typeof facets === "object") {
1222
+ return Object.entries(facets).map(([name, count]) => ({
1223
+ name,
1224
+ count: typeof count === "number" ? count : Number(count) || 0
1225
+ }));
1226
+ }
1227
+ return [];
1228
+ }
1229
+ function registerGroupTools(server2) {
1230
+ server2.registerTool(
1231
+ "ckan_group_list",
1232
+ {
1233
+ title: "List CKAN Groups",
1234
+ description: `List all groups on a CKAN server.
1235
+
1236
+ Groups are thematic collections of datasets.
1237
+
1238
+ Args:
1239
+ - server_url (string): Base URL of CKAN server
1240
+ - all_fields (boolean): Return full objects vs just names (default: false)
1241
+ - sort (string): Sort field (default: "name asc")
1242
+ - limit (number): Maximum results (default: 100). Use 0 to get only the count via faceting
1243
+ - offset (number): Pagination offset (default: 0)
1244
+ - response_format ('markdown' | 'json'): Output format
1245
+
1246
+ Returns:
1247
+ List of groups with metadata. When limit=0, returns only the count of groups with datasets.`,
1248
+ inputSchema: z7.object({
1249
+ server_url: z7.string().url(),
1250
+ all_fields: z7.boolean().optional().default(false),
1251
+ sort: z7.string().optional().default("name asc"),
1252
+ limit: z7.number().int().min(0).optional().default(100),
1253
+ offset: z7.number().int().min(0).optional().default(0),
1254
+ response_format: ResponseFormatSchema
1255
+ }).strict(),
1256
+ annotations: {
1257
+ readOnlyHint: true,
1258
+ destructiveHint: false,
1259
+ idempotentHint: true,
1260
+ openWorldHint: false
1261
+ }
1262
+ },
1263
+ async (params) => {
1264
+ try {
1265
+ if (params.limit === 0) {
1266
+ const searchResult = await makeCkanRequest(
1267
+ params.server_url,
1268
+ "package_search",
1269
+ {
1270
+ rows: 0,
1271
+ "facet.field": JSON.stringify(["groups"]),
1272
+ "facet.limit": -1
1273
+ }
1274
+ );
1275
+ const groupCount = searchResult.search_facets?.groups?.items?.length || 0;
1276
+ if (params.response_format === "json" /* JSON */) {
1277
+ return {
1278
+ content: [{ type: "text", text: JSON.stringify({ count: groupCount }, null, 2) }],
1279
+ structuredContent: { count: groupCount }
1280
+ };
1281
+ }
1282
+ const markdown2 = `# CKAN Groups Count
1283
+
1284
+ **Server**: ${params.server_url}
1285
+ **Total groups (with datasets)**: ${groupCount}
1286
+ `;
1287
+ return {
1288
+ content: [{ type: "text", text: markdown2 }]
1289
+ };
1290
+ }
1291
+ const result = await makeCkanRequest(
1292
+ params.server_url,
1293
+ "group_list",
1294
+ {
1295
+ all_fields: params.all_fields,
1296
+ sort: params.sort,
1297
+ limit: params.limit,
1298
+ offset: params.offset
1299
+ }
1300
+ );
1301
+ if (params.response_format === "json" /* JSON */) {
1302
+ const output = Array.isArray(result) ? { count: result.length, groups: result } : result;
1303
+ return {
1304
+ content: [{ type: "text", text: truncateText(JSON.stringify(output, null, 2)) }],
1305
+ structuredContent: output
1306
+ };
1307
+ }
1308
+ let markdown = `# CKAN Groups
1309
+
1310
+ `;
1311
+ markdown += `**Server**: ${params.server_url}
1312
+ `;
1313
+ markdown += `**Total**: ${Array.isArray(result) ? result.length : "Unknown"}
1314
+
1315
+ `;
1316
+ if (Array.isArray(result)) {
1317
+ if (params.all_fields) {
1318
+ for (const group of result) {
1319
+ markdown += `## ${group.title || group.name}
1320
+
1321
+ `;
1322
+ markdown += `- **ID**: \`${group.id}\`
1323
+ `;
1324
+ markdown += `- **Name**: \`${group.name}\`
1325
+ `;
1326
+ if (group.description) markdown += `- **Description**: ${group.description.substring(0, 200)}
1327
+ `;
1328
+ markdown += `- **Datasets**: ${group.package_count || 0}
1329
+ `;
1330
+ markdown += `- **Created**: ${formatDate(group.created)}
1331
+ `;
1332
+ markdown += `- **Link**: ${getGroupViewUrl(params.server_url, group)}
1333
+
1334
+ `;
1335
+ }
1336
+ } else {
1337
+ markdown += result.map((name) => `- ${name}`).join("\n");
1338
+ }
1339
+ }
1340
+ return {
1341
+ content: [{ type: "text", text: truncateText(markdown) }]
1342
+ };
1343
+ } catch (error) {
1344
+ return {
1345
+ content: [{
1346
+ type: "text",
1347
+ text: `Error listing groups: ${error instanceof Error ? error.message : String(error)}`
1348
+ }],
1349
+ isError: true
1350
+ };
1351
+ }
1352
+ }
1353
+ );
1354
+ server2.registerTool(
1355
+ "ckan_group_show",
1356
+ {
1357
+ title: "Show CKAN Group Details",
1358
+ description: `Get details of a specific group.
1359
+
1360
+ Args:
1361
+ - server_url (string): Base URL of CKAN server
1362
+ - id (string): Group ID or name
1363
+ - include_datasets (boolean): Include list of datasets (default: true)
1364
+ - response_format ('markdown' | 'json'): Output format
1365
+
1366
+ Returns:
1367
+ Group details with optional datasets`,
1368
+ inputSchema: z7.object({
1369
+ server_url: z7.string().url(),
1370
+ id: z7.string().min(1),
1371
+ include_datasets: z7.boolean().optional().default(true),
1372
+ response_format: ResponseFormatSchema
1373
+ }).strict(),
1374
+ annotations: {
1375
+ readOnlyHint: true,
1376
+ destructiveHint: false,
1377
+ idempotentHint: true,
1378
+ openWorldHint: false
1379
+ }
1380
+ },
1381
+ async (params) => {
1382
+ try {
1383
+ const result = await makeCkanRequest(
1384
+ params.server_url,
1385
+ "group_show",
1386
+ {
1387
+ id: params.id,
1388
+ include_datasets: params.include_datasets
1389
+ }
1390
+ );
1391
+ if (params.response_format === "json" /* JSON */) {
1392
+ return {
1393
+ content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
1394
+ structuredContent: result
1395
+ };
1396
+ }
1397
+ let markdown = `# Group: ${result.title || result.name}
1398
+
1399
+ `;
1400
+ markdown += `**Server**: ${params.server_url}
1401
+ `;
1402
+ markdown += `**Link**: ${getGroupViewUrl(params.server_url, result)}
1403
+
1404
+ `;
1405
+ markdown += `## Details
1406
+
1407
+ `;
1408
+ markdown += `- **ID**: \`${result.id}\`
1409
+ `;
1410
+ markdown += `- **Name**: \`${result.name}\`
1411
+ `;
1412
+ markdown += `- **Datasets**: ${result.package_count || 0}
1413
+ `;
1414
+ markdown += `- **Created**: ${formatDate(result.created)}
1415
+ `;
1416
+ markdown += `- **State**: ${result.state}
1417
+
1418
+ `;
1419
+ if (result.description) {
1420
+ markdown += `## Description
1421
+
1422
+ ${result.description}
1423
+
1424
+ `;
1425
+ }
1426
+ if (result.packages && result.packages.length > 0) {
1427
+ markdown += `## Datasets (${result.packages.length})
1428
+
1429
+ `;
1430
+ for (const pkg of result.packages.slice(0, 20)) {
1431
+ markdown += `- **${pkg.title || pkg.name}** (\`${pkg.name}\`)
1432
+ `;
1433
+ }
1434
+ if (result.packages.length > 20) {
1435
+ markdown += `
1436
+ ... and ${result.packages.length - 20} more datasets
1437
+ `;
1438
+ }
1439
+ markdown += "\n";
1440
+ }
1441
+ return {
1442
+ content: [{ type: "text", text: truncateText(markdown) }]
1443
+ };
1444
+ } catch (error) {
1445
+ return {
1446
+ content: [{
1447
+ type: "text",
1448
+ text: `Error fetching group: ${error instanceof Error ? error.message : String(error)}`
1449
+ }],
1450
+ isError: true
1451
+ };
1452
+ }
1453
+ }
1454
+ );
1455
+ server2.registerTool(
1456
+ "ckan_group_search",
1457
+ {
1458
+ title: "Search CKAN Groups by Name",
1459
+ description: `Search for groups by name pattern.
1460
+
1461
+ This tool provides a simpler interface than package_search for finding groups.
1462
+ Wildcards are automatically added around the search pattern.
1463
+
1464
+ Args:
1465
+ - server_url (string): Base URL of CKAN server
1466
+ - pattern (string): Search pattern (e.g., "energia", "salute")
1467
+ - response_format ('markdown' | 'json'): Output format
1468
+
1469
+ Returns:
1470
+ List of matching groups with dataset counts`,
1471
+ inputSchema: z7.object({
1472
+ server_url: z7.string().url(),
1473
+ pattern: z7.string().min(1).describe("Search pattern (wildcards added automatically)"),
1474
+ response_format: ResponseFormatSchema
1475
+ }).strict(),
1476
+ annotations: {
1477
+ readOnlyHint: true,
1478
+ destructiveHint: false,
1479
+ idempotentHint: true,
1480
+ openWorldHint: true
1481
+ }
1482
+ },
1483
+ async (params) => {
1484
+ try {
1485
+ const query = `groups:*${params.pattern}*`;
1486
+ const result = await makeCkanRequest(
1487
+ params.server_url,
1488
+ "package_search",
1489
+ {
1490
+ q: query,
1491
+ rows: 0,
1492
+ "facet.field": JSON.stringify(["groups"]),
1493
+ "facet.limit": 500
1494
+ }
1495
+ );
1496
+ const groupFacets = normalizeGroupFacets(result);
1497
+ const totalDatasets = result.count || 0;
1498
+ if (params.response_format === "json" /* JSON */) {
1499
+ const jsonResult = {
1500
+ count: groupFacets.length,
1501
+ total_datasets: totalDatasets,
1502
+ groups: groupFacets.map((group) => ({
1503
+ name: group.name,
1504
+ display_name: group.display_name,
1505
+ dataset_count: group.count
1506
+ }))
1507
+ };
1508
+ return {
1509
+ content: [{ type: "text", text: truncateText(JSON.stringify(jsonResult, null, 2)) }],
1510
+ structuredContent: jsonResult
1511
+ };
1512
+ }
1513
+ let markdown = `# CKAN Group Search Results
1514
+
1515
+ `;
1516
+ markdown += `**Server**: ${params.server_url}
1517
+ `;
1518
+ markdown += `**Pattern**: "${params.pattern}"
1519
+ `;
1520
+ markdown += `**Groups Found**: ${groupFacets.length}
1521
+ `;
1522
+ markdown += `**Total Datasets**: ${totalDatasets}
1523
+
1524
+ `;
1525
+ if (groupFacets.length === 0) {
1526
+ markdown += `No groups found matching pattern "${params.pattern}".
1527
+ `;
1528
+ } else {
1529
+ markdown += `## Matching Groups
1530
+
1531
+ `;
1532
+ markdown += `| Group | Datasets |
1533
+ `;
1534
+ markdown += `|-------|----------|
1535
+ `;
1536
+ for (const group of groupFacets) {
1537
+ markdown += `| ${group.display_name || group.name} | ${group.count} |
1538
+ `;
1539
+ }
1540
+ }
1541
+ return {
1542
+ content: [{ type: "text", text: truncateText(markdown) }]
1543
+ };
1544
+ } catch (error) {
1545
+ return {
1546
+ content: [{
1547
+ type: "text",
1548
+ text: `Error searching groups: ${error instanceof Error ? error.message : String(error)}`
1549
+ }],
1550
+ isError: true
1551
+ };
1552
+ }
1553
+ }
1554
+ );
1555
+ }
1556
+
1062
1557
  // src/resources/dataset.ts
1063
1558
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1064
1559
 
@@ -1232,7 +1727,7 @@ function registerAllResources(server2) {
1232
1727
  function createServer() {
1233
1728
  return new McpServer({
1234
1729
  name: "ckan-mcp-server",
1235
- version: "0.4.2"
1730
+ version: "0.4.3"
1236
1731
  });
1237
1732
  }
1238
1733
  function registerAll(server2) {
@@ -1240,6 +1735,8 @@ function registerAll(server2) {
1240
1735
  registerOrganizationTools(server2);
1241
1736
  registerDatastoreTools(server2);
1242
1737
  registerStatusTools(server2);
1738
+ registerTagTools(server2);
1739
+ registerGroupTools(server2);
1243
1740
  registerAllResources(server2);
1244
1741
  }
1245
1742