@aborruso/ckan-mcp-server 0.4.73 → 0.4.77

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
@@ -1,5 +1,18 @@
1
1
  # LOG
2
2
 
3
+ ## 2026-03-07 (v0.4.77)
4
+
5
+ - fix(`http.ts`): remove `Referer`, `Sec-Fetch-*`, `Upgrade-Insecure-Requests` from axios headers — these triggered WAF block on BA Data (data.buenosaires.gob.ar) and other portals with strict WAF rules; dati.gov.it unaffected
6
+
7
+ ## 2026-03-06 (v0.4.75)
8
+
9
+ - fix(`ckan_find_portals`): deduplicate portals by hostname, preferring https over http
10
+ - feat: new tool `ckan_find_portals` — discovers CKAN portals from datashades.info registry (~950 portals); filters by country, keyword, min_datasets, language, has_datastore; LLM translates country to English
11
+
12
+ ## 2026-03-06 (v0.4.74)
13
+
14
+ - fix: use `z.coerce.number()` for all numeric tool parameters — fixes validation errors when MCP clients pass numbers as strings (closes #16)
15
+
3
16
  ## 2026-03-05 (v0.4.73)
4
17
 
5
18
  - feat: `package_show` now includes `api_json_url` for dataset and each resource (direct CKAN API JSON link)
package/dist/index.js CHANGED
@@ -359,11 +359,6 @@ async function makeCkanRequest(serverUrl, action, params = {}) {
359
359
  "Accept-Language": "en-US,en;q=0.9,it;q=0.8",
360
360
  "Accept-Encoding": "gzip, deflate, br",
361
361
  Connection: "keep-alive",
362
- Referer: `${baseUrl}/`,
363
- "Sec-Fetch-Site": "same-origin",
364
- "Sec-Fetch-Mode": "navigate",
365
- "Sec-Fetch-Dest": "document",
366
- "Upgrade-Insecure-Requests": "1",
367
362
  "Sec-CH-UA": '"Chromium";v="120", "Not?A_Brand";v="24", "Google Chrome";v="120"',
368
363
  "Sec-CH-UA-Mobile": "?0",
369
364
  "Sec-CH-UA-Platform": '"Linux"',
@@ -1062,16 +1057,16 @@ Typical workflow: ckan_package_search \u2192 ckan_package_show (get full metadat
1062
1057
  server_url: z2.string().url("Must be a valid URL").describe("Base URL of the CKAN server"),
1063
1058
  q: z2.string().optional().default("*:*").describe("Search query in Solr syntax"),
1064
1059
  fq: z2.string().optional().describe(`Filter query in Solr syntax; applied after scoring, does not affect relevance. CKAN extras fields use prefix 'extras_' (e.g. extras_hvd_category). For OR on same field use field:(val1 OR val2), never field:val1 OR field:val2 (silently breaks). Examples: 'organization:comune-palermo', 'res_format:CSV', 'extras_hvd_category:("uri1" OR "uri2")'.`),
1065
- rows: z2.number().int().min(0).max(1e3).optional().default(10).describe("Number of results to return"),
1066
- start: z2.number().int().min(0).optional().default(0).describe("Offset for pagination"),
1060
+ rows: z2.coerce.number().int().min(0).max(1e3).optional().default(10).describe("Number of results to return"),
1061
+ start: z2.coerce.number().int().min(0).optional().default(0).describe("Offset for pagination"),
1067
1062
  sort: z2.string().optional().describe("Sort field and direction (e.g., 'metadata_modified desc')"),
1068
1063
  facet_field: z2.array(z2.string()).optional().describe("Fields to facet on"),
1069
- facet_limit: z2.number().int().min(1).optional().default(50).describe("Maximum facet values per field"),
1070
- page: z2.number().int().min(1).optional().describe("Page number (1-based); alias for start. Overrides start if provided."),
1071
- page_size: z2.number().int().min(1).max(1e3).optional().default(10).describe("Results per page when using page (default: 10)"),
1064
+ facet_limit: z2.coerce.number().int().min(1).optional().default(50).describe("Maximum facet values per field"),
1065
+ page: z2.coerce.number().int().min(1).optional().describe("Page number (1-based); alias for start. Overrides start if provided."),
1066
+ page_size: z2.coerce.number().int().min(1).max(1e3).optional().default(10).describe("Results per page when using page (default: 10)"),
1072
1067
  include_drafts: z2.boolean().optional().default(false).describe("Include draft datasets"),
1073
1068
  content_recent: z2.boolean().optional().default(false).describe("Use issued date with fallback to metadata_created for recent content"),
1074
- content_recent_days: z2.number().int().min(1).optional().default(30).describe("Day window for content_recent (default 30)"),
1069
+ content_recent_days: z2.coerce.number().int().min(1).optional().default(30).describe("Day window for content_recent (default 30)"),
1075
1070
  query_parser: z2.enum(["default", "text"]).optional().describe("Override search parser ('text' forces text:(...) on non-fielded queries)"),
1076
1071
  response_format: ResponseFormatSchema
1077
1072
  }).strict(),
@@ -1312,12 +1307,12 @@ Typical workflow: ckan_find_relevant_datasets \u2192 ckan_package_show (inspect
1312
1307
  inputSchema: z2.object({
1313
1308
  server_url: z2.string().url().describe("Base URL of the CKAN server (e.g., https://dati.gov.it/opendata)"),
1314
1309
  query: z2.string().min(2).describe("Natural language or keyword query to match against dataset title, notes, tags, and organization"),
1315
- limit: z2.number().int().min(1).max(50).optional().default(10).describe("Number of datasets to return"),
1310
+ limit: z2.coerce.number().int().min(1).max(50).optional().default(10).describe("Number of datasets to return"),
1316
1311
  weights: z2.object({
1317
- title: z2.number().min(0).optional().describe("Weight for title match (default 4)"),
1318
- notes: z2.number().min(0).optional().describe("Weight for description match (default 2)"),
1319
- tags: z2.number().min(0).optional().describe("Weight for tag match (default 3)"),
1320
- organization: z2.number().min(0).optional().describe("Weight for organization match (default 1)")
1312
+ title: z2.coerce.number().min(0).optional().describe("Weight for title match (default 4)"),
1313
+ notes: z2.coerce.number().min(0).optional().describe("Weight for description match (default 2)"),
1314
+ tags: z2.coerce.number().min(0).optional().describe("Weight for tag match (default 3)"),
1315
+ organization: z2.coerce.number().min(0).optional().describe("Weight for organization match (default 1)")
1321
1316
  }).optional().describe("Per-field scoring weights; unspecified fields use defaults"),
1322
1317
  query_parser: z2.enum(["default", "text"]).optional().describe("Override search parser ('text' forces text:(...) on non-fielded queries)"),
1323
1318
  response_format: ResponseFormatSchema
@@ -1795,8 +1790,8 @@ Typical workflow: ckan_organization_list \u2192 ckan_organization_show (inspect
1795
1790
  server_url: z3.string().url().describe("Base URL of the CKAN server (e.g., https://dati.gov.it/opendata)"),
1796
1791
  all_fields: z3.boolean().optional().default(false).describe("Return full organization objects (true) or just name slugs (false)"),
1797
1792
  sort: z3.string().optional().default("name asc").describe("Sort field and direction (e.g., 'name asc', 'package_count desc')"),
1798
- limit: z3.number().int().min(0).optional().default(100).describe("Max organizations to return. Use 0 to get only the count via faceting"),
1799
- offset: z3.number().int().min(0).optional().default(0).describe("Pagination offset"),
1793
+ limit: z3.coerce.number().int().min(0).optional().default(100).describe("Max organizations to return. Use 0 to get only the count via faceting"),
1794
+ offset: z3.coerce.number().int().min(0).optional().default(0).describe("Pagination offset"),
1800
1795
  response_format: ResponseFormatSchema
1801
1796
  }).strict(),
1802
1797
  annotations: {
@@ -2301,8 +2296,8 @@ Typical workflow: ckan_package_search \u2192 ckan_package_show (find resource_id
2301
2296
  resource_id: z4.string().min(1).describe("UUID of the DataStore resource (from ckan_package_show resource.id where datastore_active is true)"),
2302
2297
  q: z4.string().optional().describe("Full-text search across all fields"),
2303
2298
  filters: z4.record(z4.any()).optional().describe('Key-value filters for exact matches (e.g., { "regione": "Sicilia", "anno": 2023 })'),
2304
- limit: z4.number().int().min(0).max(32e3).optional().default(100).describe("Max rows to return (default 100, max 32000); use 0 to get only column names without data"),
2305
- offset: z4.number().int().min(0).optional().default(0).describe("Pagination offset"),
2299
+ limit: z4.coerce.number().int().min(0).max(32e3).optional().default(100).describe("Max rows to return (default 100, max 32000); use 0 to get only column names without data"),
2300
+ offset: z4.coerce.number().int().min(0).optional().default(0).describe("Pagination offset"),
2306
2301
  fields: z4.array(z4.string()).optional().describe("Specific field names to return; omit to return all fields"),
2307
2302
  sort: z4.string().optional().describe("Sort expression (e.g., 'anno desc', 'nome asc')"),
2308
2303
  distinct: z4.boolean().optional().default(false).describe("Return only distinct rows"),
@@ -4215,6 +4210,132 @@ ${error instanceof Error ? error.message : String(error)}`
4215
4210
  );
4216
4211
  }
4217
4212
 
4213
+ // src/tools/portal-discovery.ts
4214
+ import { z as z11 } from "zod";
4215
+ import axios3 from "axios";
4216
+ var DATASHADES_URL = "https://datashades.info/api/portal/list";
4217
+ async function fetchPortals() {
4218
+ const resp = await axios3.get(DATASHADES_URL, {
4219
+ timeout: 15e3,
4220
+ headers: { "User-Agent": "CKAN-MCP-Server/1.0" }
4221
+ });
4222
+ return resp.data.portals;
4223
+ }
4224
+ function deduplicateByHostname(portals) {
4225
+ const seen = /* @__PURE__ */ new Map();
4226
+ for (const p of portals) {
4227
+ try {
4228
+ const hostname = new URL(p.Href).hostname;
4229
+ const existing = seen.get(hostname);
4230
+ if (!existing || p.Href.startsWith("https://")) {
4231
+ seen.set(hostname, p);
4232
+ }
4233
+ } catch {
4234
+ }
4235
+ }
4236
+ return Array.from(seen.values());
4237
+ }
4238
+ function filterPortals(portals, params) {
4239
+ const filtered = portals.filter((p) => p.status === "active" && p.Href).filter((p) => !params.country || p.Coordinates.country_name.toLowerCase().includes(params.country.toLowerCase())).filter((p) => !params.query || p.SiteInfo.site_title.toLowerCase().includes(params.query.toLowerCase())).filter((p) => params.min_datasets === void 0 || p.DatasetsNumber >= params.min_datasets).filter((p) => !params.language || p.SiteInfo.locale_default.toLowerCase().startsWith(params.language.toLowerCase())).filter((p) => !params.has_datastore || (p.Plugins || []).includes("datastore"));
4240
+ return deduplicateByHostname(filtered).sort((a, b) => b.DatasetsNumber - a.DatasetsNumber).slice(0, params.limit);
4241
+ }
4242
+ function formatMarkdown(portals, total, limit) {
4243
+ if (portals.length === 0) return "No CKAN portals found matching the given filters.";
4244
+ const rows = portals.map(
4245
+ (p) => `| [${p.SiteInfo.site_title || p.Href}](${p.Href}) | ${p.Coordinates.country_name} | ${p.Version} | ${p.DatasetsNumber.toLocaleString()} | ${p.SiteInfo.locale_default} | ${(p.Plugins || []).includes("datastore") ? "\u2705" : "\u274C"} |`
4246
+ ).join("\n");
4247
+ return `# CKAN Portals
4248
+
4249
+ **Source**: [datashades.info](https://datashades.info/portals) \u2014 live registry of ${total} active portals
4250
+ **Showing**: ${portals.length} of ${total} (filtered, sorted by dataset count)
4251
+
4252
+ | Portal | Country | CKAN | Datasets | Locale | DataStore |
4253
+ |--------|---------|------|----------|--------|-----------|
4254
+ ${rows}
4255
+
4256
+ ---
4257
+ \u{1F4A1} Use the portal URL as \`server_url\` in any CKAN tool.`;
4258
+ }
4259
+ function registerPortalDiscoveryTools(server2) {
4260
+ server2.registerTool(
4261
+ "ckan_find_portals",
4262
+ {
4263
+ title: "Find CKAN Portals",
4264
+ description: `Search the live datashades.info registry of ~950 CKAN portals worldwide.
4265
+
4266
+ Use this tool to discover which CKAN portals exist for a country, language, or topic
4267
+ before querying them with other CKAN tools.
4268
+
4269
+ **IMPORTANT \u2014 country parameter**: always pass country name in English.
4270
+ If the user writes in another language (e.g. "Italia", "Espa\xF1a", "Brasil"),
4271
+ translate to English ("Italy", "Spain", "Brazil") before calling this tool.
4272
+
4273
+ Args:
4274
+ - country (string): Country name in English (e.g. "Italy", "Brazil", "France")
4275
+ - query (string): Keyword to match against portal title (e.g. "transport", "health")
4276
+ - min_datasets (number): Minimum number of datasets (e.g. 100)
4277
+ - language (string): Portal default locale code (e.g. "it", "en", "pt_BR", "fr")
4278
+ - has_datastore (boolean): If true, return only portals with DataStore enabled (supports SQL queries)
4279
+ - limit (number): Max results to return (default 10, max 50)
4280
+
4281
+ Returns:
4282
+ Ranked list of matching portals with URL, country, CKAN version, dataset count, and DataStore status.
4283
+
4284
+ Typical workflow: ckan_find_portals (discover portal URL) \u2192 ckan_status_show (verify) \u2192 ckan_package_search (search datasets)`,
4285
+ inputSchema: z11.object({
4286
+ country: z11.string().optional().describe("Country name in English (e.g. 'Italy', 'Brazil'). Translate from any language before passing."),
4287
+ query: z11.string().optional().describe("Keyword matched against portal title (case-insensitive)"),
4288
+ min_datasets: z11.coerce.number().int().min(0).optional().describe("Minimum number of datasets"),
4289
+ language: z11.string().optional().describe("Portal default locale code (e.g. 'it', 'en', 'pt_BR')"),
4290
+ has_datastore: z11.boolean().optional().describe("If true, return only portals with DataStore plugin (required for SQL queries)"),
4291
+ limit: z11.coerce.number().int().min(1).max(50).optional().default(10).describe("Max results (default 10, max 50)")
4292
+ }).strict(),
4293
+ annotations: {
4294
+ readOnlyHint: true,
4295
+ destructiveHint: false,
4296
+ idempotentHint: true,
4297
+ openWorldHint: true
4298
+ }
4299
+ },
4300
+ async (params) => {
4301
+ try {
4302
+ const all = await fetchPortals();
4303
+ const active = all.filter((p) => p.status === "active");
4304
+ const results = filterPortals(all, {
4305
+ country: params.country,
4306
+ query: params.query,
4307
+ min_datasets: params.min_datasets,
4308
+ language: params.language,
4309
+ has_datastore: params.has_datastore,
4310
+ limit: params.limit
4311
+ });
4312
+ const markdown = formatMarkdown(results, active.length, params.limit);
4313
+ return {
4314
+ content: [{ type: "text", text: addDemoFooter(markdown) }],
4315
+ structuredContent: { portals: results.map((p) => ({
4316
+ url: p.Href,
4317
+ title: p.SiteInfo.site_title,
4318
+ country: p.Coordinates.country_name,
4319
+ version: p.Version,
4320
+ datasets: p.DatasetsNumber,
4321
+ locale: p.SiteInfo.locale_default,
4322
+ has_datastore: (p.Plugins || []).includes("datastore")
4323
+ })) }
4324
+ };
4325
+ } catch (error) {
4326
+ return {
4327
+ content: [{
4328
+ type: "text",
4329
+ text: `Could not fetch portal list from datashades.info:
4330
+ ${error instanceof Error ? error.message : String(error)}`
4331
+ }],
4332
+ isError: true
4333
+ };
4334
+ }
4335
+ }
4336
+ );
4337
+ }
4338
+
4218
4339
  // src/resources/dataset.ts
4219
4340
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4220
4341
 
@@ -4484,7 +4605,7 @@ function registerAllResources(server2) {
4484
4605
  }
4485
4606
 
4486
4607
  // src/prompts/theme.ts
4487
- import { z as z11 } from "zod";
4608
+ import { z as z12 } from "zod";
4488
4609
 
4489
4610
  // src/prompts/types.ts
4490
4611
  var createTextPrompt = (text) => ({
@@ -4545,9 +4666,9 @@ var registerThemePrompt = (server2) => {
4545
4666
  title: "Search datasets by theme",
4546
4667
  description: "Guided prompt to discover a theme and search datasets under it.",
4547
4668
  argsSchema: {
4548
- server_url: z11.string().url().describe("Base URL of the CKAN server"),
4549
- theme: z11.string().min(1).describe("Theme or group name to search"),
4550
- rows: z11.coerce.number().int().positive().default(10).describe("Max results to return")
4669
+ server_url: z12.string().url().describe("Base URL of the CKAN server"),
4670
+ theme: z12.string().min(1).describe("Theme or group name to search"),
4671
+ rows: z12.coerce.number().int().positive().default(10).describe("Max results to return")
4551
4672
  }
4552
4673
  },
4553
4674
  async ({ server_url, theme, rows }) => createTextPrompt(buildThemePromptText(server_url, theme, rows))
@@ -4555,7 +4676,7 @@ var registerThemePrompt = (server2) => {
4555
4676
  };
4556
4677
 
4557
4678
  // src/prompts/organization.ts
4558
- import { z as z12 } from "zod";
4679
+ import { z as z13 } from "zod";
4559
4680
  var ORGANIZATION_PROMPT_NAME = "ckan-search-by-organization";
4560
4681
  var buildOrganizationPromptText = (serverUrl, organization, rows) => `# Guided search: datasets by organization
4561
4682
 
@@ -4590,9 +4711,9 @@ var registerOrganizationPrompt = (server2) => {
4590
4711
  title: "Search datasets by organization",
4591
4712
  description: "Guided prompt to find a publisher and list its datasets.",
4592
4713
  argsSchema: {
4593
- server_url: z12.string().url().describe("Base URL of the CKAN server"),
4594
- organization: z12.string().min(1).describe("Organization name or keyword"),
4595
- rows: z12.coerce.number().int().positive().default(10).describe("Max results to return")
4714
+ server_url: z13.string().url().describe("Base URL of the CKAN server"),
4715
+ organization: z13.string().min(1).describe("Organization name or keyword"),
4716
+ rows: z13.coerce.number().int().positive().default(10).describe("Max results to return")
4596
4717
  }
4597
4718
  },
4598
4719
  async ({ server_url, organization, rows }) => createTextPrompt(buildOrganizationPromptText(server_url, organization, rows))
@@ -4600,7 +4721,7 @@ var registerOrganizationPrompt = (server2) => {
4600
4721
  };
4601
4722
 
4602
4723
  // src/prompts/format.ts
4603
- import { z as z13 } from "zod";
4724
+ import { z as z14 } from "zod";
4604
4725
  var FORMAT_PROMPT_NAME = "ckan-search-by-format";
4605
4726
  var buildFormatPromptText = (serverUrl, format, rows) => `# Guided search: datasets by resource format
4606
4727
 
@@ -4624,9 +4745,9 @@ var registerFormatPrompt = (server2) => {
4624
4745
  title: "Search datasets by resource format",
4625
4746
  description: "Guided prompt to find datasets with a given resource format.",
4626
4747
  argsSchema: {
4627
- server_url: z13.string().url().describe("Base URL of the CKAN server"),
4628
- format: z13.string().min(1).describe("Resource format (e.g., CSV, JSON)"),
4629
- rows: z13.coerce.number().int().positive().default(10).describe("Max results to return")
4748
+ server_url: z14.string().url().describe("Base URL of the CKAN server"),
4749
+ format: z14.string().min(1).describe("Resource format (e.g., CSV, JSON)"),
4750
+ rows: z14.coerce.number().int().positive().default(10).describe("Max results to return")
4630
4751
  }
4631
4752
  },
4632
4753
  async ({ server_url, format, rows }) => createTextPrompt(buildFormatPromptText(server_url, format, rows))
@@ -4634,7 +4755,7 @@ var registerFormatPrompt = (server2) => {
4634
4755
  };
4635
4756
 
4636
4757
  // src/prompts/recent.ts
4637
- import { z as z14 } from "zod";
4758
+ import { z as z15 } from "zod";
4638
4759
  var RECENT_PROMPT_NAME = "ckan-recent-datasets";
4639
4760
  var buildRecentPromptText = (serverUrl, rows) => `# Guided search: recent datasets
4640
4761
 
@@ -4682,8 +4803,8 @@ var registerRecentPrompt = (server2) => {
4682
4803
  title: "Find recently updated datasets",
4683
4804
  description: "Guided prompt to list recently updated datasets on a CKAN portal.",
4684
4805
  argsSchema: {
4685
- server_url: z14.string().url().describe("Base URL of the CKAN server"),
4686
- rows: z14.coerce.number().int().positive().default(10).describe("Max results to return")
4806
+ server_url: z15.string().url().describe("Base URL of the CKAN server"),
4807
+ rows: z15.coerce.number().int().positive().default(10).describe("Max results to return")
4687
4808
  }
4688
4809
  },
4689
4810
  async ({ server_url, rows }) => createTextPrompt(buildRecentPromptText(server_url, rows))
@@ -4691,7 +4812,7 @@ var registerRecentPrompt = (server2) => {
4691
4812
  };
4692
4813
 
4693
4814
  // src/prompts/dataset-analysis.ts
4694
- import { z as z15 } from "zod";
4815
+ import { z as z16 } from "zod";
4695
4816
  var DATASET_ANALYSIS_PROMPT_NAME = "ckan-analyze-dataset";
4696
4817
  var buildDatasetAnalysisPromptText = (serverUrl, id) => `# Guided analysis: dataset
4697
4818
 
@@ -4733,8 +4854,8 @@ var registerDatasetAnalysisPrompt = (server2) => {
4733
4854
  title: "Analyze a dataset",
4734
4855
  description: "Guided prompt to inspect dataset metadata and explore DataStore tables.",
4735
4856
  argsSchema: {
4736
- server_url: z15.string().url().describe("Base URL of the CKAN server"),
4737
- id: z15.string().min(1).describe("Dataset id or name (CKAN package id)")
4857
+ server_url: z16.string().url().describe("Base URL of the CKAN server"),
4858
+ id: z16.string().min(1).describe("Dataset id or name (CKAN package id)")
4738
4859
  }
4739
4860
  },
4740
4861
  async ({ server_url, id }) => createTextPrompt(buildDatasetAnalysisPromptText(server_url, id))
@@ -4742,7 +4863,7 @@ var registerDatasetAnalysisPrompt = (server2) => {
4742
4863
  };
4743
4864
 
4744
4865
  // src/prompts/hvd.ts
4745
- import { z as z16 } from "zod";
4866
+ import { z as z17 } from "zod";
4746
4867
  var HVD_PROMPT_NAME = "ckan-search-hvd";
4747
4868
  var buildHvdPromptText = (serverUrl, rows, categoryField) => {
4748
4869
  if (!categoryField) {
@@ -4804,8 +4925,8 @@ var registerHvdPrompt = (server2) => {
4804
4925
  title: "Search High-Value Datasets (HVD)",
4805
4926
  description: "Guided prompt to find High-Value Datasets (HVD) on a CKAN portal. Automatically uses the correct filter field from portal configuration.",
4806
4927
  argsSchema: {
4807
- server_url: z16.string().url().describe("Base URL of the CKAN server"),
4808
- rows: z16.coerce.number().int().positive().default(10).describe("Max results to return")
4928
+ server_url: z17.string().url().describe("Base URL of the CKAN server"),
4929
+ rows: z17.coerce.number().int().positive().default(10).describe("Max results to return")
4809
4930
  }
4810
4931
  },
4811
4932
  async ({ server_url, rows }) => {
@@ -4829,7 +4950,7 @@ var registerAllPrompts = (server2) => {
4829
4950
  function createServer() {
4830
4951
  return new McpServer({
4831
4952
  name: "ckan-mcp-server",
4832
- version: "0.4.73"
4953
+ version: "0.4.77"
4833
4954
  });
4834
4955
  }
4835
4956
  function registerAll(server2) {
@@ -4843,6 +4964,7 @@ function registerAll(server2) {
4843
4964
  registerAnalyzeTools(server2);
4844
4965
  registerCatalogStatsTools(server2);
4845
4966
  registerSparqlTools(server2);
4967
+ registerPortalDiscoveryTools(server2);
4846
4968
  registerAllResources(server2);
4847
4969
  registerAllPrompts(server2);
4848
4970
  }