@aborruso/ckan-mcp-server 0.4.49 → 0.4.51

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
@@ -489,6 +489,24 @@ function convertDateMathForUnsupportedFields(query) {
489
489
  return `${field}:[${startIso} TO ${nowIso}]`;
490
490
  });
491
491
  }
492
+ var EXPLICIT_BOOL_PATTERN = /\b(AND|OR|NOT)\b|[+\-!]/;
493
+ function isPlainMultiTermQuery(query) {
494
+ const trimmed = query.trim();
495
+ if (trimmed === "*:*" || trimmed === "") return false;
496
+ if (FIELD_QUERY_PATTERN.test(trimmed)) return false;
497
+ if (EXPLICIT_BOOL_PATTERN.test(trimmed)) return false;
498
+ const words = trimmed.split(/\s+/).filter(Boolean);
499
+ return words.length > 1;
500
+ }
501
+ function buildOrQuery(query) {
502
+ return query.trim().split(/\s+/).filter(Boolean).join(" OR ");
503
+ }
504
+ function stripAccents(text) {
505
+ return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
506
+ }
507
+ function hasAccents(text) {
508
+ return text !== stripAccents(text);
509
+ }
492
510
  function resolveSearchQuery(serverUrl, query, parserOverride) {
493
511
  const portalSearchConfig = getPortalSearchConfig(serverUrl);
494
512
  const portalForce = portalSearchConfig.force_text_field ?? false;
@@ -761,8 +779,14 @@ ${result.notes}
761
779
  `;
762
780
  if (resource.last_modified) markdown += `- **Modified**: ${formatDate(resource.last_modified)}
763
781
  `;
764
- if (resource.datastore_active !== void 0) {
765
- markdown += `- **DataStore**: ${resource.datastore_active ? "\u2705 Available" : "\u274C Not available"}
782
+ if (resource.datastore_active === true) {
783
+ markdown += `- **DataStore**: \u2705 Available
784
+ `;
785
+ } else if (resource.datastore_active === false) {
786
+ markdown += `- **DataStore**: \u274C Not available
787
+ `;
788
+ } else {
789
+ markdown += `- **DataStore**: \u2753 Not reported by portal
766
790
  `;
767
791
  }
768
792
  markdown += "\n";
@@ -780,6 +804,12 @@ ${result.notes}
780
804
  }
781
805
  return markdown;
782
806
  };
807
+ function resolvePageParams(page, pageSize, start, rows) {
808
+ if (page !== void 0) {
809
+ return { effectiveStart: (page - 1) * pageSize, effectiveRows: pageSize };
810
+ }
811
+ return { effectiveStart: start, effectiveRows: rows };
812
+ }
783
813
  function registerPackageTools(server2) {
784
814
  server2.registerTool(
785
815
  "ckan_package_search",
@@ -819,6 +849,8 @@ Args:
819
849
  - fq (string): Filter query (e.g., "organization:comune-palermo")
820
850
  - rows (number): Number of results to return (default: 10, max: 1000)
821
851
  - start (number): Offset for pagination (default: 0)
852
+ - page (number): Page number (1-based); alias for start. Overrides start if provided.
853
+ - page_size (number): Results per page when using page (default: 10, max: 1000)
822
854
  - sort (string): Sort field and direction (e.g., "metadata_modified desc")
823
855
  - facet_field (array): Fields to facet on (e.g., ["organization", "tags"])
824
856
  - facet_limit (number): Max facet values per field (default: 50)
@@ -899,6 +931,8 @@ Typical workflow: ckan_package_search \u2192 ckan_package_show (get full metadat
899
931
  sort: z2.string().optional().describe("Sort field and direction (e.g., 'metadata_modified desc')"),
900
932
  facet_field: z2.array(z2.string()).optional().describe("Fields to facet on"),
901
933
  facet_limit: z2.number().int().min(1).optional().default(50).describe("Maximum facet values per field"),
934
+ page: z2.number().int().min(1).optional().describe("Page number (1-based); alias for start. Overrides start if provided."),
935
+ page_size: z2.number().int().min(1).max(1e3).optional().default(10).describe("Results per page when using page (default: 10)"),
902
936
  include_drafts: z2.boolean().optional().default(false).describe("Include draft datasets"),
903
937
  content_recent: z2.boolean().optional().default(false).describe("Use issued date with fallback to metadata_created for recent content"),
904
938
  content_recent_days: z2.number().int().min(1).optional().default(30).describe("Day window for content_recent (default 30)"),
@@ -928,10 +962,11 @@ Typical workflow: ckan_package_search \u2192 ckan_package_show (get full metadat
928
962
  query,
929
963
  params.query_parser
930
964
  );
965
+ const { effectiveRows, effectiveStart } = resolvePageParams(params.page, params.page_size, params.start, params.rows);
931
966
  const apiParams = {
932
967
  q: effectiveQuery,
933
- rows: params.rows,
934
- start: params.start,
968
+ rows: effectiveRows,
969
+ start: effectiveStart,
935
970
  include_private: params.include_drafts
936
971
  };
937
972
  if (params.fq) apiParams.fq = params.fq;
@@ -940,11 +975,29 @@ Typical workflow: ckan_package_search \u2192 ckan_package_show (get full metadat
940
975
  apiParams["facet.field"] = JSON.stringify(params.facet_field);
941
976
  apiParams["facet.limit"] = params.facet_limit;
942
977
  }
943
- const result = await makeCkanRequest(
978
+ let result = await makeCkanRequest(
944
979
  params.server_url,
945
980
  "package_search",
946
981
  apiParams
947
982
  );
983
+ let accentFallbackUsed = false;
984
+ if (result.count === 0 && hasAccents(params.q)) {
985
+ const strippedQuery = stripAccents(params.q);
986
+ const { effectiveQuery: strippedEffective } = resolveSearchQuery(
987
+ params.server_url,
988
+ strippedQuery,
989
+ params.query_parser
990
+ );
991
+ const fallbackResult = await makeCkanRequest(
992
+ params.server_url,
993
+ "package_search",
994
+ { ...apiParams, q: strippedEffective }
995
+ );
996
+ if (fallbackResult.count > 0) {
997
+ result = fallbackResult;
998
+ accentFallbackUsed = true;
999
+ }
1000
+ }
948
1001
  if (params.response_format === "json" /* JSON */) {
949
1002
  return {
950
1003
  content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }]
@@ -958,10 +1011,12 @@ ${params.content_recent ? `**Content Recent**: last ${params.content_recent_days
958
1011
  ` : ""}
959
1012
  ${effectiveQuery !== userQuery ? `**Effective Query**: ${effectiveQuery}
960
1013
  ` : ""}
1014
+ ${accentFallbackUsed ? `**Note**: Original query returned 0 results; retried with accent-stripped query "${stripAccents(params.q)}".
1015
+ ` : ""}
961
1016
  ${params.fq ? `**Filter**: ${params.fq}
962
1017
  ` : ""}
963
1018
  **Total Results**: ${result.count}
964
- **Showing**: ${result.results.length} results (from ${params.start})
1019
+ **Showing**: ${result.results.length} results (from ${effectiveStart})
965
1020
 
966
1021
  `;
967
1022
  if (result.facets && Object.keys(result.facets).length > 0) {
@@ -1027,13 +1082,27 @@ Note: showing top ${sorted.length} only. Use \`response_format: json\` for full
1027
1082
  } else {
1028
1083
  markdown += `No datasets found matching your query.
1029
1084
  `;
1085
+ if (isPlainMultiTermQuery(params.q)) {
1086
+ markdown += `
1087
+ > **Tip**: Multi-term queries use AND by default (all terms must match). Try OR to broaden the search:
1088
+ `;
1089
+ markdown += `> \`q: "${buildOrQuery(params.q)}"\`
1090
+ `;
1091
+ }
1030
1092
  }
1031
- if (result.count > params.start + params.rows) {
1032
- const nextStart = params.start + params.rows;
1033
- markdown += `
1093
+ if (result.count > effectiveStart + effectiveRows) {
1094
+ if (params.page !== void 0) {
1095
+ markdown += `
1096
+ ---
1097
+ **More results available**: Use \`page: ${params.page + 1}\` to see next page.
1098
+ `;
1099
+ } else {
1100
+ const nextStart = effectiveStart + effectiveRows;
1101
+ markdown += `
1034
1102
  ---
1035
1103
  **More results available**: Use \`start: ${nextStart}\` to see next page.
1036
1104
  `;
1105
+ }
1037
1106
  }
1038
1107
  return {
1039
1108
  content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
@@ -1319,6 +1388,7 @@ Use this to quickly assess what files a dataset contains before deciding how to
1319
1388
  Args:
1320
1389
  - server_url (string): Base URL of CKAN server
1321
1390
  - id (string): Dataset ID or name
1391
+ - format_filter (string): Filter resources by format, case-insensitive (e.g., "CSV", "json", "XLSX")
1322
1392
  - response_format ('markdown' | 'json'): Output format
1323
1393
 
1324
1394
  Returns:
@@ -1326,11 +1396,13 @@ Returns:
1326
1396
 
1327
1397
  Examples:
1328
1398
  - { server_url: "https://dati.gov.it/opendata", id: "dataset-name" }
1399
+ - { server_url: "...", id: "dataset-name", format_filter: "CSV" }
1329
1400
 
1330
1401
  Typical workflow: ckan_package_search \u2192 ckan_list_resources (assess available files) \u2192 ckan_datastore_search (for resources with DataStore=true)`,
1331
1402
  inputSchema: z2.object({
1332
1403
  server_url: z2.string().url().describe("Base URL of the CKAN server"),
1333
1404
  id: z2.string().min(1).describe("Dataset ID or name"),
1405
+ format_filter: z2.string().optional().describe("Filter resources by format, case-insensitive (e.g., 'CSV', 'json', 'XLSX')"),
1334
1406
  response_format: ResponseFormatSchema
1335
1407
  }).strict(),
1336
1408
  annotations: {
@@ -1348,7 +1420,8 @@ Typical workflow: ckan_package_search \u2192 ckan_list_resources (assess availab
1348
1420
  { id: params.id }
1349
1421
  );
1350
1422
  const resources = Array.isArray(result.resources) ? result.resources : [];
1351
- const summary = resources.map((r) => {
1423
+ const formatFilter = params.format_filter?.toUpperCase();
1424
+ const summary = resources.filter((r) => !formatFilter || (r.format || "").toUpperCase() === formatFilter).map((r) => {
1352
1425
  const effectiveUrl = resolveDownloadUrl(r);
1353
1426
  return {
1354
1427
  name: r.name || "Unnamed Resource",
@@ -1364,7 +1437,9 @@ Typical workflow: ckan_package_search \u2192 ckan_list_resources (assess availab
1364
1437
  dataset_id: result.id,
1365
1438
  dataset_name: result.name,
1366
1439
  dataset_title: result.title || result.name,
1367
- total_resources: summary.length,
1440
+ total_resources: resources.length,
1441
+ filtered_resources: summary.length,
1442
+ format_filter: formatFilter ?? null,
1368
1443
  resources: summary
1369
1444
  };
1370
1445
  return {
@@ -1379,7 +1454,11 @@ Typical workflow: ckan_package_search \u2192 ckan_list_resources (assess availab
1379
1454
  `;
1380
1455
  markdown += `**Dataset**: \`${result.name}\` (\`${result.id}\`)
1381
1456
  `;
1382
- markdown += `**Total Resources**: ${summary.length}
1457
+ markdown += `**Total Resources**: ${resources.length}`;
1458
+ if (formatFilter) {
1459
+ markdown += ` (showing ${summary.length} ${formatFilter})`;
1460
+ }
1461
+ markdown += `
1383
1462
 
1384
1463
  `;
1385
1464
  if (summary.length === 0) {
@@ -1426,6 +1505,65 @@ Typical workflow: ckan_package_search \u2192 ckan_list_resources (assess availab
1426
1505
 
1427
1506
  // src/tools/organization.ts
1428
1507
  import { z as z3 } from "zod";
1508
+ function formatOrganizationShowMarkdown(result, serverUrl) {
1509
+ let markdown = `# Organization: ${result.title || result.name}
1510
+
1511
+ `;
1512
+ markdown += `**Server**: ${serverUrl}
1513
+ `;
1514
+ markdown += `**Link**: ${getOrganizationViewUrl(serverUrl, result)}
1515
+
1516
+ `;
1517
+ markdown += `## Details
1518
+
1519
+ `;
1520
+ markdown += `- **ID**: \`${result.id}\`
1521
+ `;
1522
+ markdown += `- **Name**: \`${result.name}\`
1523
+ `;
1524
+ markdown += `- **Datasets**: ${result.package_count || 0}
1525
+ `;
1526
+ markdown += `- **Created**: ${formatDate(result.created)}
1527
+ `;
1528
+ markdown += `- **State**: ${result.state}
1529
+
1530
+ `;
1531
+ if (result.description) {
1532
+ markdown += `## Description
1533
+
1534
+ ${result.description}
1535
+
1536
+ `;
1537
+ }
1538
+ if (result.packages && result.packages.length > 0) {
1539
+ const displayed = Math.min(result.packages.length, 20);
1540
+ const totalHint = result.package_count && result.package_count !== result.packages.length ? ` \u2014 ${result.package_count} total` : "";
1541
+ markdown += `## Datasets (showing ${displayed} of ${result.packages.length} returned${totalHint})
1542
+
1543
+ `;
1544
+ for (const pkg of result.packages.slice(0, 20)) {
1545
+ markdown += `- **${pkg.title || pkg.name}** (\`${pkg.name}\`)
1546
+ `;
1547
+ }
1548
+ if (result.packages.length > 20) {
1549
+ markdown += `
1550
+ ... and ${result.packages.length - 20} more datasets
1551
+ `;
1552
+ }
1553
+ markdown += "\n";
1554
+ }
1555
+ if (result.users && result.users.length > 0) {
1556
+ markdown += `## Users (${result.users.length})
1557
+
1558
+ `;
1559
+ for (const user of result.users) {
1560
+ markdown += `- **${user.name}** (${user.capacity})
1561
+ `;
1562
+ }
1563
+ markdown += "\n";
1564
+ }
1565
+ return markdown;
1566
+ }
1429
1567
  function registerOrganizationTools(server2) {
1430
1568
  server2.registerTool(
1431
1569
  "ckan_organization_list",
@@ -1669,60 +1807,7 @@ Typical workflow: ckan_organization_show \u2192 ckan_package_show (inspect a dat
1669
1807
  structuredContent: result
1670
1808
  };
1671
1809
  }
1672
- let markdown = `# Organization: ${result.title || result.name}
1673
-
1674
- `;
1675
- markdown += `**Server**: ${params.server_url}
1676
- `;
1677
- markdown += `**Link**: ${getOrganizationViewUrl(params.server_url, result)}
1678
-
1679
- `;
1680
- markdown += `## Details
1681
-
1682
- `;
1683
- markdown += `- **ID**: \`${result.id}\`
1684
- `;
1685
- markdown += `- **Name**: \`${result.name}\`
1686
- `;
1687
- markdown += `- **Datasets**: ${result.package_count || 0}
1688
- `;
1689
- markdown += `- **Created**: ${formatDate(result.created)}
1690
- `;
1691
- markdown += `- **State**: ${result.state}
1692
-
1693
- `;
1694
- if (result.description) {
1695
- markdown += `## Description
1696
-
1697
- ${result.description}
1698
-
1699
- `;
1700
- }
1701
- if (result.packages && result.packages.length > 0) {
1702
- markdown += `## Datasets (${result.packages.length})
1703
-
1704
- `;
1705
- for (const pkg of result.packages.slice(0, 20)) {
1706
- markdown += `- **${pkg.title || pkg.name}** (\`${pkg.name}\`)
1707
- `;
1708
- }
1709
- if (result.packages.length > 20) {
1710
- markdown += `
1711
- ... and ${result.packages.length - 20} more datasets
1712
- `;
1713
- }
1714
- markdown += "\n";
1715
- }
1716
- if (result.users && result.users.length > 0) {
1717
- markdown += `## Users (${result.users.length})
1718
-
1719
- `;
1720
- for (const user of result.users) {
1721
- markdown += `- **${user.name}** (${user.capacity})
1722
- `;
1723
- }
1724
- markdown += "\n";
1725
- }
1810
+ const markdown = formatOrganizationShowMarkdown(result, params.server_url);
1726
1811
  return {
1727
1812
  content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1728
1813
  };
@@ -1847,6 +1932,108 @@ Typical workflow: ckan_organization_search \u2192 ckan_organization_show (get de
1847
1932
 
1848
1933
  // src/tools/datastore.ts
1849
1934
  import { z as z4 } from "zod";
1935
+ function formatDatastoreSearchMarkdown(result, serverUrl, resourceId, offset, limit) {
1936
+ let markdown = `# DataStore Query Results
1937
+
1938
+ `;
1939
+ markdown += `**Server**: ${serverUrl}
1940
+ `;
1941
+ markdown += `**Resource ID**: \`${resourceId}\`
1942
+ `;
1943
+ markdown += `**Total Records**: ${result.total || 0}
1944
+ `;
1945
+ markdown += `**Returned**: ${result.records ? result.records.length : 0} records
1946
+
1947
+ `;
1948
+ if (result.fields && result.fields.length > 0) {
1949
+ markdown += `## Fields
1950
+
1951
+ `;
1952
+ markdown += result.fields.map((f) => `- **${f.id}** (${f.type})`).join("\n") + "\n\n";
1953
+ }
1954
+ if (result.records && result.records.length > 0) {
1955
+ markdown += `## Records
1956
+
1957
+ `;
1958
+ const fields = result.fields ? result.fields.map((f) => f.id).filter((id) => id !== "_id") : [];
1959
+ const displayFields = fields.slice(0, 8);
1960
+ markdown += `| ${displayFields.join(" | ")} |
1961
+ `;
1962
+ markdown += `| ${displayFields.map(() => "---").join(" | ")} |
1963
+ `;
1964
+ for (const record of result.records.slice(0, 50)) {
1965
+ const values = displayFields.map((field) => {
1966
+ const val = record[field];
1967
+ if (val === null || val === void 0) return "-";
1968
+ const str = String(val);
1969
+ return str.length > 80 ? str.substring(0, 77) + "..." : str;
1970
+ });
1971
+ markdown += `| ${values.join(" | ")} |
1972
+ `;
1973
+ }
1974
+ if (result.records.length > 50) {
1975
+ markdown += `
1976
+ ... and ${result.records.length - 50} more records
1977
+ `;
1978
+ }
1979
+ markdown += "\n";
1980
+ }
1981
+ if (result.total && result.total > offset + (result.records?.length || 0)) {
1982
+ const nextOffset = offset + limit;
1983
+ markdown += `**More results available**: Use \`offset: ${nextOffset}\` for next page.
1984
+ `;
1985
+ }
1986
+ return markdown;
1987
+ }
1988
+ function formatDatastoreSqlMarkdown(result, serverUrl, sql) {
1989
+ const records = result.records || [];
1990
+ const fieldIds = (result.fields?.map((field) => field.id) || Object.keys(records[0] || {})).filter((id) => id !== "_id");
1991
+ let markdown = `# DataStore SQL Results
1992
+
1993
+ `;
1994
+ markdown += `**Server**: ${serverUrl}
1995
+ `;
1996
+ markdown += `**SQL**: \`${sql}\`
1997
+ `;
1998
+ markdown += `**Returned**: ${records.length} records
1999
+
2000
+ `;
2001
+ if (result.fields && result.fields.length > 0) {
2002
+ markdown += `## Fields
2003
+
2004
+ `;
2005
+ markdown += result.fields.map((field) => `- **${field.id}** (${field.type})`).join("\n") + "\n\n";
2006
+ }
2007
+ if (records.length > 0 && fieldIds.length > 0) {
2008
+ markdown += `## Records
2009
+
2010
+ `;
2011
+ const displayFields = fieldIds.slice(0, 8);
2012
+ markdown += `| ${displayFields.join(" | ")} |
2013
+ `;
2014
+ markdown += `| ${displayFields.map(() => "---").join(" | ")} |
2015
+ `;
2016
+ for (const record of records.slice(0, 50)) {
2017
+ const values = displayFields.map((field) => {
2018
+ const value = record[field];
2019
+ if (value === null || value === void 0) return "-";
2020
+ const text = String(value);
2021
+ return text.length > 80 ? text.substring(0, 77) + "..." : text;
2022
+ });
2023
+ markdown += `| ${values.join(" | ")} |
2024
+ `;
2025
+ }
2026
+ if (records.length > 50) {
2027
+ markdown += `
2028
+ ... and ${records.length - 50} more records
2029
+ `;
2030
+ }
2031
+ markdown += "\n";
2032
+ } else {
2033
+ markdown += "No records returned by the SQL query.\n";
2034
+ }
2035
+ return markdown;
2036
+ }
1850
2037
  function registerDatastoreTools(server2) {
1851
2038
  server2.registerTool(
1852
2039
  "ckan_datastore_search",
@@ -1918,56 +2105,7 @@ Typical workflow: ckan_package_search \u2192 ckan_package_show (find resource_id
1918
2105
  content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }]
1919
2106
  };
1920
2107
  }
1921
- let markdown = `# DataStore Query Results
1922
-
1923
- `;
1924
- markdown += `**Server**: ${params.server_url}
1925
- `;
1926
- markdown += `**Resource ID**: \`${params.resource_id}\`
1927
- `;
1928
- markdown += `**Total Records**: ${result.total || 0}
1929
- `;
1930
- markdown += `**Returned**: ${result.records ? result.records.length : 0} records
1931
-
1932
- `;
1933
- if (result.fields && result.fields.length > 0) {
1934
- markdown += `## Fields
1935
-
1936
- `;
1937
- markdown += result.fields.map((f) => `- **${f.id}** (${f.type})`).join("\n") + "\n\n";
1938
- }
1939
- if (result.records && result.records.length > 0) {
1940
- markdown += `## Records
1941
-
1942
- `;
1943
- const fields = result.fields.map((f) => f.id);
1944
- const displayFields = fields.slice(0, 8);
1945
- markdown += `| ${displayFields.join(" | ")} |
1946
- `;
1947
- markdown += `| ${displayFields.map(() => "---").join(" | ")} |
1948
- `;
1949
- for (const record of result.records.slice(0, 50)) {
1950
- const values = displayFields.map((field) => {
1951
- const val = record[field];
1952
- if (val === null || val === void 0) return "-";
1953
- const str = String(val);
1954
- return str.length > 50 ? str.substring(0, 47) + "..." : str;
1955
- });
1956
- markdown += `| ${values.join(" | ")} |
1957
- `;
1958
- }
1959
- if (result.records.length > 50) {
1960
- markdown += `
1961
- ... and ${result.records.length - 50} more records
1962
- `;
1963
- }
1964
- markdown += "\n";
1965
- }
1966
- if (result.total && result.total > params.offset + (result.records?.length || 0)) {
1967
- const nextOffset = params.offset + params.limit;
1968
- markdown += `**More results available**: Use \`offset: ${nextOffset}\` for next page.
1969
- `;
1970
- }
2108
+ const markdown = formatDatastoreSearchMarkdown(result, params.server_url, params.resource_id, params.offset, params.limit);
1971
2109
  return {
1972
2110
  content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1973
2111
  };
@@ -2028,52 +2166,7 @@ Typical workflow: ckan_package_show (get resource_id) \u2192 ckan_datastore_sear
2028
2166
  structuredContent: result
2029
2167
  };
2030
2168
  }
2031
- const records = result.records || [];
2032
- const fieldIds = result.fields?.map((field) => field.id) || Object.keys(records[0] || {});
2033
- let markdown = `# DataStore SQL Results
2034
-
2035
- `;
2036
- markdown += `**Server**: ${params.server_url}
2037
- `;
2038
- markdown += `**SQL**: \`${params.sql}\`
2039
- `;
2040
- markdown += `**Returned**: ${records.length} records
2041
-
2042
- `;
2043
- if (result.fields && result.fields.length > 0) {
2044
- markdown += `## Fields
2045
-
2046
- `;
2047
- markdown += result.fields.map((field) => `- **${field.id}** (${field.type})`).join("\n") + "\n\n";
2048
- }
2049
- if (records.length > 0 && fieldIds.length > 0) {
2050
- markdown += `## Records
2051
-
2052
- `;
2053
- const displayFields = fieldIds.slice(0, 8);
2054
- markdown += `| ${displayFields.join(" | ")} |
2055
- `;
2056
- markdown += `| ${displayFields.map(() => "---").join(" | ")} |
2057
- `;
2058
- for (const record of records.slice(0, 50)) {
2059
- const values = displayFields.map((field) => {
2060
- const value = record[field];
2061
- if (value === null || value === void 0) return "-";
2062
- const text = String(value);
2063
- return text.length > 50 ? text.substring(0, 47) + "..." : text;
2064
- });
2065
- markdown += `| ${values.join(" | ")} |
2066
- `;
2067
- }
2068
- if (records.length > 50) {
2069
- markdown += `
2070
- ... and ${records.length - 50} more records
2071
- `;
2072
- }
2073
- markdown += "\n";
2074
- } else {
2075
- markdown += "No records returned by the SQL query.\n";
2076
- }
2169
+ const markdown = formatDatastoreSqlMarkdown(result, params.server_url, params.sql);
2077
2170
  return {
2078
2171
  content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
2079
2172
  };
@@ -2092,6 +2185,16 @@ Typical workflow: ckan_package_show (get resource_id) \u2192 ckan_datastore_sear
2092
2185
 
2093
2186
  // src/tools/status.ts
2094
2187
  import { z as z5 } from "zod";
2188
+ function formatStatusMarkdown(result, serverUrl) {
2189
+ return `# CKAN Server Status
2190
+
2191
+ **Server**: ${serverUrl}
2192
+ **Status**: \u2705 Online
2193
+ **CKAN Version**: ${result.ckan_version || "Unknown"}
2194
+ **Site Title**: ${result.site_title || "N/A"}
2195
+ **Site URL**: ${result.site_url || "N/A"}
2196
+ `;
2197
+ }
2095
2198
  function registerStatusTools(server2) {
2096
2199
  server2.registerTool(
2097
2200
  "ckan_status_show",
@@ -2125,14 +2228,7 @@ Typical workflow: ckan_status_show (verify server is up) \u2192 ckan_package_sea
2125
2228
  "status_show",
2126
2229
  {}
2127
2230
  );
2128
- const markdown = `# CKAN Server Status
2129
-
2130
- **Server**: ${params.server_url}
2131
- **Status**: \u2705 Online
2132
- **CKAN Version**: ${result.ckan_version || "Unknown"}
2133
- **Site Title**: ${result.site_title || "N/A"}
2134
- **Site URL**: ${result.site_url || "N/A"}
2135
- `;
2231
+ const markdown = formatStatusMarkdown(result, params.server_url);
2136
2232
  return {
2137
2233
  content: [{ type: "text", text: addDemoFooter(markdown) }],
2138
2234
  structuredContent: result
@@ -2154,7 +2250,10 @@ ${error instanceof Error ? error.message : String(error)}`
2154
2250
  // src/tools/tag.ts
2155
2251
  import { z as z6 } from "zod";
2156
2252
  function normalizeTagFacets(result) {
2157
- const searchItems = result?.search_facets?.tags?.items;
2253
+ const r = result;
2254
+ const searchFacets = r?.search_facets;
2255
+ const tagsGroup = searchFacets?.tags;
2256
+ const searchItems = tagsGroup?.items;
2158
2257
  if (Array.isArray(searchItems)) {
2159
2258
  return searchItems.map((item) => ({
2160
2259
  name: item?.name || item?.display_name || String(item),
@@ -2162,7 +2261,7 @@ function normalizeTagFacets(result) {
2162
2261
  display_name: item?.display_name
2163
2262
  }));
2164
2263
  }
2165
- const facets = result?.facets?.tags;
2264
+ const facets = r?.facets?.tags;
2166
2265
  if (Array.isArray(facets)) {
2167
2266
  if (facets.length > 0 && typeof facets[0] === "object") {
2168
2267
  return facets.map((item) => ({
@@ -2292,16 +2391,67 @@ function getGroupViewUrl(serverUrl, group) {
2292
2391
  const cleanServerUrl = serverUrl.replace(/\/$/, "");
2293
2392
  return `${cleanServerUrl}/group/${group.name}`;
2294
2393
  }
2394
+ function formatGroupShowMarkdown(result, serverUrl) {
2395
+ let markdown = `# Group: ${result.title || result.name}
2396
+
2397
+ `;
2398
+ markdown += `**Server**: ${serverUrl}
2399
+ `;
2400
+ markdown += `**Link**: ${getGroupViewUrl(serverUrl, result)}
2401
+
2402
+ `;
2403
+ markdown += `## Details
2404
+
2405
+ `;
2406
+ markdown += `- **ID**: \`${result.id}\`
2407
+ `;
2408
+ markdown += `- **Name**: \`${result.name}\`
2409
+ `;
2410
+ markdown += `- **Datasets**: ${result.package_count || 0}
2411
+ `;
2412
+ markdown += `- **Created**: ${formatDate(result.created)}
2413
+ `;
2414
+ markdown += `- **State**: ${result.state}
2415
+
2416
+ `;
2417
+ if (result.description) {
2418
+ markdown += `## Description
2419
+
2420
+ ${result.description}
2421
+
2422
+ `;
2423
+ }
2424
+ if (result.packages && result.packages.length > 0) {
2425
+ const displayed = Math.min(result.packages.length, 20);
2426
+ const totalHint = result.package_count && result.package_count !== result.packages.length ? ` \u2014 ${result.package_count} total` : "";
2427
+ markdown += `## Datasets (showing ${displayed} of ${result.packages.length} returned${totalHint})
2428
+
2429
+ `;
2430
+ for (const pkg of result.packages.slice(0, 20)) {
2431
+ markdown += `- **${pkg.title || pkg.name}** (\`${pkg.name}\`)
2432
+ `;
2433
+ }
2434
+ if (result.packages.length > 20) {
2435
+ markdown += `
2436
+ ... and ${result.packages.length - 20} more datasets
2437
+ `;
2438
+ }
2439
+ markdown += "\n";
2440
+ }
2441
+ return markdown;
2442
+ }
2295
2443
  function normalizeGroupFacets(result) {
2296
- const items = result?.search_facets?.groups?.items;
2297
- if (Array.isArray(items)) {
2298
- return items.map((item) => ({
2444
+ const r = result;
2445
+ const items = r?.search_facets?.groups;
2446
+ const itemsArr = items?.items;
2447
+ if (Array.isArray(itemsArr)) {
2448
+ return itemsArr.map((item) => ({
2299
2449
  name: item?.name || item?.display_name || String(item),
2300
2450
  display_name: item?.display_name,
2301
2451
  count: typeof item?.count === "number" ? item.count : 0
2302
2452
  }));
2303
2453
  }
2304
- const facets = result?.facets?.groups;
2454
+ const facets = r?.facets?.groups;
2305
2455
  if (Array.isArray(facets)) {
2306
2456
  if (facets.length > 0 && typeof facets[0] === "object") {
2307
2457
  return facets.map((item) => ({
@@ -2492,50 +2642,7 @@ Typical workflow: ckan_group_show \u2192 ckan_package_show (inspect a dataset) \
2492
2642
  structuredContent: result
2493
2643
  };
2494
2644
  }
2495
- let markdown = `# Group: ${result.title || result.name}
2496
-
2497
- `;
2498
- markdown += `**Server**: ${params.server_url}
2499
- `;
2500
- markdown += `**Link**: ${getGroupViewUrl(params.server_url, result)}
2501
-
2502
- `;
2503
- markdown += `## Details
2504
-
2505
- `;
2506
- markdown += `- **ID**: \`${result.id}\`
2507
- `;
2508
- markdown += `- **Name**: \`${result.name}\`
2509
- `;
2510
- markdown += `- **Datasets**: ${result.package_count || 0}
2511
- `;
2512
- markdown += `- **Created**: ${formatDate(result.created)}
2513
- `;
2514
- markdown += `- **State**: ${result.state}
2515
-
2516
- `;
2517
- if (result.description) {
2518
- markdown += `## Description
2519
-
2520
- ${result.description}
2521
-
2522
- `;
2523
- }
2524
- if (result.packages && result.packages.length > 0) {
2525
- markdown += `## Datasets (${result.packages.length})
2526
-
2527
- `;
2528
- for (const pkg of result.packages.slice(0, 20)) {
2529
- markdown += `- **${pkg.title || pkg.name}** (\`${pkg.name}\`)
2530
- `;
2531
- }
2532
- if (result.packages.length > 20) {
2533
- markdown += `
2534
- ... and ${result.packages.length - 20} more datasets
2535
- `;
2536
- }
2537
- markdown += "\n";
2538
- }
2645
+ const markdown = formatGroupShowMarkdown(result, params.server_url);
2539
2646
  return {
2540
2647
  content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
2541
2648
  };
@@ -3089,8 +3196,9 @@ function metricAvailability(section, availabilityKey, statusKey) {
3089
3196
  return void 0;
3090
3197
  }
3091
3198
  function normalizeQualityData(data) {
3092
- const mqaData = data?.mqa ?? data;
3093
- const breakdown = data?.breakdown;
3199
+ const d = data;
3200
+ const mqaData = d?.mqa ?? data;
3201
+ const breakdown = d?.breakdown;
3094
3202
  const resultEntry = mqaData?.result?.results?.[0];
3095
3203
  if (!resultEntry || typeof resultEntry !== "object") {
3096
3204
  return { ...data, breakdown };
@@ -3896,7 +4004,7 @@ var registerAllPrompts = (server2) => {
3896
4004
  function createServer() {
3897
4005
  return new McpServer({
3898
4006
  name: "ckan-mcp-server",
3899
- version: "0.4.49"
4007
+ version: "0.4.50"
3900
4008
  });
3901
4009
  }
3902
4010
  function registerAll(server2) {