@aborruso/ckan-mcp-server 0.4.1 โ†’ 0.4.3

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/LOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## 2026-01-10
4
4
 
5
+ ### Version 0.4.3 - Tags and Groups
6
+ - **Tags**: Added `ckan_tag_list` with faceting and filtering
7
+ - **Groups**: Added `ckan_group_list`, `ckan_group_show`, `ckan_group_search`
8
+ - **Docs**: Updated README with examples and tool list
9
+ - **Tests**: Added tag/group fixtures and tests
10
+
11
+ ### Version 0.4.2 - Packaging
12
+ - **npm package**: Added `.npmignore` to exclude dev artifacts
13
+
5
14
  ### Version 0.4.1 - Maintenance
6
15
  - **Date formatting**: ISO `YYYY-MM-DD` output, tests aligned
7
16
  - **HTTP transport**: Single shared transport per process
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # CKAN MCP Server
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@aborruso/ckan-mcp-server)](https://www.npmjs.com/package/@aborruso/ckan-mcp-server)
4
+
3
5
  MCP (Model Context Protocol) server for interacting with CKAN-based open data portals.
4
6
 
5
7
  ## Features
@@ -12,14 +14,14 @@ MCP (Model Context Protocol) server for interacting with CKAN-based open data po
12
14
  - ๐ŸŽจ Output in Markdown or JSON format
13
15
  - โšก Pagination and faceting support
14
16
  - ๐Ÿ“„ MCP Resource Templates for direct data access
15
- - ๐Ÿงช Comprehensive test suite (113 tests, 100% passing)
17
+ - ๐Ÿงช Comprehensive test suite (120 tests, 100% passing)
16
18
 
17
19
  ## Installation
18
20
 
19
21
  ### From npm (recommended)
20
22
 
21
23
  ```bash
22
- npm install ckan-mcp-server
24
+ npm install -g @aborruso/ckan-mcp-server
23
25
  ```
24
26
 
25
27
  ### From source
@@ -34,7 +36,7 @@ npm install
34
36
  # Build with esbuild (fast, ~4ms)
35
37
  npm run build
36
38
 
37
- # Run tests (113 tests)
39
+ # Run tests (120 tests)
38
40
  npm test
39
41
  ```
40
42
 
@@ -76,38 +78,19 @@ TRANSPORT=http PORT=3000 npm start
76
78
 
77
79
  **Best for**: Global access, zero infrastructure, free hosting
78
80
 
79
- Deploy to Cloudflare's edge network for worldwide low-latency access.
80
-
81
- **Prerequisites**:
82
- - Cloudflare account (free): https://dash.cloudflare.com/sign-up
83
- - Wrangler CLI: `npm install -g wrangler`
84
-
85
- **Quick Deploy**:
86
-
87
- ```bash
88
- # Clone repository
89
- git clone https://github.com/aborruso/ckan-mcp-server.git
90
- cd ckan-mcp-server
91
-
92
- # Install dependencies
93
- npm install
94
-
95
- # Authenticate with Cloudflare
96
- wrangler login
81
+ Use the public Workers endpoint (no local install required):
97
82
 
98
- # Deploy to Workers
99
- npm run deploy
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "ckan": {
87
+ "url": "https://ckan-mcp-server.andy-pr.workers.dev/mcp"
88
+ }
89
+ }
90
+ }
100
91
  ```
101
92
 
102
- Your server will be live at: `https://ckan-mcp-server.<your-account>.workers.dev`
103
-
104
- **Free tier includes**:
105
- - 100,000 requests/day
106
- - Global edge deployment
107
- - Automatic HTTPS
108
- - No cold starts
109
-
110
- For detailed deployment instructions, see [DEPLOYMENT.md](docs/DEPLOYMENT.md).
93
+ Want your own deployment? See [DEPLOYMENT.md](docs/DEPLOYMENT.md).
111
94
 
112
95
  ## Claude Desktop Configuration
113
96
 
@@ -137,22 +120,16 @@ Then add to `claude_desktop_config.json`:
137
120
  }
138
121
  ```
139
122
 
140
- ### Option 2: Local Installation
141
-
142
- Install in a specific project:
143
-
144
- ```bash
145
- npm install @aborruso/ckan-mcp-server
146
- ```
123
+ ### Option 2: Local Installation (Optional)
147
124
 
148
- Then add to `claude_desktop_config.json`:
125
+ If you installed locally (see Installation), use this config:
149
126
 
150
127
  ```json
151
128
  {
152
129
  "mcpServers": {
153
130
  "ckan": {
154
131
  "command": "node",
155
- "args": ["/absolute/path/to/project/node_modules/@aborruso/ckan-mcp-server/dist/index.js"]
132
+ "args": ["/absolute/path/to/project/node_modules/@username/ckan-mcp-server/dist/index.js"]
156
133
  }
157
134
  }
158
135
  }
@@ -198,6 +175,7 @@ Use the public Cloudflare Workers deployment (no local installation required):
198
175
  - **ckan_package_search**: Search datasets with Solr queries
199
176
  - **ckan_package_show**: Complete details of a dataset
200
177
  - **ckan_package_list**: List all datasets
178
+ - **ckan_tag_list**: List tags with counts
201
179
 
202
180
  ### Organizations
203
181
 
@@ -209,6 +187,12 @@ Use the public Cloudflare Workers deployment (no local installation required):
209
187
  - **ckan_datastore_search**: Query tabular data
210
188
  - **ckan_datastore_search_sql**: SQL queries (in development)
211
189
 
190
+ ### Groups
191
+
192
+ - **ckan_group_list**: List groups
193
+ - **ckan_group_show**: Show group details
194
+ - **ckan_group_search**: Search groups by name
195
+
212
196
  ### Utilities
213
197
 
214
198
  - **ckan_status_show**: Verify server status
@@ -274,6 +258,25 @@ ckan_package_search({
274
258
  })
275
259
  ```
276
260
 
261
+ ### List tags (natural language: "show top tags about health")
262
+
263
+ ```typescript
264
+ ckan_tag_list({
265
+ server_url: "https://www.dati.gov.it/opendata",
266
+ tag_query: "salute",
267
+ limit: 25
268
+ })
269
+ ```
270
+
271
+ ### Search groups (natural language: "find groups about environment")
272
+
273
+ ```typescript
274
+ ckan_group_search({
275
+ server_url: "https://www.dati.gov.it/opendata",
276
+ pattern: "ambiente"
277
+ })
278
+ ```
279
+
277
280
  ### DataStore Query
278
281
 
279
282
  ```typescript
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.1"
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