@aborruso/ckan-mcp-server 0.4.6 → 0.4.7

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/EXAMPLES.md CHANGED
@@ -50,6 +50,16 @@ ckan_package_search({
50
50
  })
51
51
  ```
52
52
 
53
+ ### Long OR query (force text-field parser)
54
+ ```typescript
55
+ ckan_package_search({
56
+ server_url: "https://www.dati.gov.it/opendata",
57
+ q: "hotel OR alberghi OR \"strutture ricettive\" OR ospitalità OR ricettività OR agriturismo OR \"bed and breakfast\"",
58
+ query_parser: "text",
59
+ rows: 0
60
+ })
61
+ ```
62
+
53
63
  ### Regione Siciliana datasets
54
64
  ```typescript
55
65
  ckan_package_search({
package/LOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## 2026-01-10
4
4
 
5
+ ### Version 0.4.7 - Portal search parser override
6
+ - **Config**: Added per-portal search parser config
7
+ - **Tool**: Added query parser override for package search and relevance
8
+
9
+ ## 2026-01-10
10
+
5
11
  ### Version 0.4.6 - Relevance ranking
6
12
  - **Tool**: Added `ckan_find_relevant_datasets`
7
13
  - **Docs**: Updated README/EXAMPLES
package/README.md CHANGED
@@ -40,27 +40,11 @@ npm run build
40
40
  npm test
41
41
  ```
42
42
 
43
- ## Usage
44
-
45
- ### Start with stdio (for local integration)
46
-
47
- ```bash
48
- npm start
49
- ```
50
-
51
- ### Start with HTTP (for remote access)
52
-
53
- ```bash
54
- TRANSPORT=http PORT=3000 npm start
55
- ```
56
-
57
- The server will be available at `http://localhost:3000/mcp`
58
-
59
43
  ## Usage Options
60
44
 
61
45
  ### Option 1: Local Installation (stdio mode)
62
46
 
63
- **Best for**: Personal use with Claude Desktop
47
+ **Best for**: Personal use with local MCP clients
64
48
 
65
49
  Install and run locally on your machine (see Installation section above).
66
50
 
@@ -92,7 +76,7 @@ Use the public Workers endpoint (no local install required):
92
76
 
93
77
  Want your own deployment? See [DEPLOYMENT.md](docs/DEPLOYMENT.md).
94
78
 
95
- ## Claude Desktop Configuration
79
+ ### Claude Desktop Configuration
96
80
 
97
81
  Configuration file location:
98
82
 
@@ -100,7 +84,7 @@ Configuration file location:
100
84
  - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
101
85
  - **Linux**: `~/.config/Claude/claude_desktop_config.json`
102
86
 
103
- ### Option 1: Global Installation (Recommended)
87
+ #### Option 1: Global Installation (Recommended)
104
88
 
105
89
  Install globally to use across all projects:
106
90
 
@@ -120,7 +104,7 @@ Then add to `claude_desktop_config.json`:
120
104
  }
121
105
  ```
122
106
 
123
- ### Option 2: Local Installation (Optional)
107
+ #### Option 2: Local Installation (Optional)
124
108
 
125
109
  If you installed locally (see Installation), use this config:
126
110
 
@@ -137,7 +121,7 @@ If you installed locally (see Installation), use this config:
137
121
 
138
122
  Replace `/absolute/path/to/project` with your actual project path.
139
123
 
140
- ### Option 3: From Source
124
+ #### Option 3: From Source
141
125
 
142
126
  If you cloned the repository:
143
127
 
@@ -152,7 +136,7 @@ If you cloned the repository:
152
136
  }
153
137
  ```
154
138
 
155
- ### Option 4: Cloudflare Workers (HTTP transport)
139
+ #### Option 4: Cloudflare Workers (HTTP transport)
156
140
 
157
141
  Use the public Cloudflare Workers deployment (no local installation required):
158
142
 
@@ -226,6 +210,17 @@ ckan_package_search({
226
210
  })
227
211
  ```
228
212
 
213
+ ### Force text-field parser for long OR queries
214
+
215
+ ```typescript
216
+ ckan_package_search({
217
+ server_url: "https://www.dati.gov.it/opendata",
218
+ q: "hotel OR alberghi OR \"strutture ricettive\" OR ospitalità OR ricettività",
219
+ query_parser: "text",
220
+ rows: 0
221
+ })
222
+ ```
223
+
229
224
  ### Rank datasets by relevance
230
225
 
231
226
  ```typescript
@@ -477,28 +472,32 @@ ckan_package_search({
477
472
  ```
478
473
  ckan-mcp-server/
479
474
  ├── src/
480
- │ ├── index.ts # Entry point
481
- │ ├── server.ts # MCP server setup
482
- │ ├── types.ts # Types & schemas
475
+ │ ├── index.ts # Entry point
476
+ │ ├── server.ts # MCP server setup
477
+ │ ├── worker.ts # Cloudflare Workers entry
478
+ │ ├── types.ts # Types & schemas
483
479
  │ ├── utils/
484
- │ │ ├── http.ts # CKAN API client
485
- │ │ └── formatting.ts # Output formatting
480
+ │ │ ├── http.ts # CKAN API client
481
+ │ │ ├── formatting.ts # Output formatting
482
+ │ │ └── url-generator.ts
486
483
  │ ├── tools/
487
- │ │ ├── package.ts # Package search/show
484
+ │ │ ├── package.ts # Package search/show
488
485
  │ │ ├── organization.ts # Organization tools
489
- │ │ ├── datastore.ts # DataStore queries
490
- │ │ └── status.ts # Server status
491
- │ ├── resources/ # MCP Resource Templates
486
+ │ │ ├── datastore.ts # DataStore queries
487
+ │ │ ├── status.ts # Server status
488
+ ├── tag.ts # Tag tools
489
+ │ │ └── group.ts # Group tools
490
+ │ ├── resources/ # MCP Resource Templates
492
491
  │ │ ├── index.ts
493
- │ │ ├── uri.ts # URI parsing
492
+ │ │ ├── uri.ts # URI parsing
494
493
  │ │ ├── dataset.ts
495
494
  │ │ ├── resource.ts
496
495
  │ │ └── organization.ts
497
496
  │ └── transport/
498
- │ ├── stdio.ts # Stdio transport
499
- │ └── http.ts # HTTP transport
500
- ├── tests/ # Test suite (113 tests)
501
- ├── dist/ # Compiled files (generated)
497
+ │ ├── stdio.ts # Stdio transport
498
+ │ └── http.ts # HTTP transport
499
+ ├── tests/ # Test suite (120 tests)
500
+ ├── dist/ # Compiled files (generated)
502
501
  ├── package.json
503
502
  └── README.md
504
503
  ```
package/dist/index.js CHANGED
@@ -90,41 +90,82 @@ var portals_default = {
90
90
  "http://www.dati.gov.it/opendata",
91
91
  "http://dati.gov.it/opendata"
92
92
  ],
93
+ search: {
94
+ force_text_field: true
95
+ },
93
96
  dataset_view_url: "https://www.dati.gov.it/view-dataset/dataset?id={id}",
94
97
  organization_view_url: "https://www.dati.gov.it/view-dataset?organization={name}"
95
98
  }
96
99
  ],
97
100
  defaults: {
98
101
  dataset_view_url: "{server_url}/dataset/{name}",
99
- organization_view_url: "{server_url}/organization/{name}"
102
+ organization_view_url: "{server_url}/organization/{name}",
103
+ search: {
104
+ force_text_field: false
105
+ }
100
106
  }
101
107
  };
102
108
 
103
- // src/utils/url-generator.ts
109
+ // src/utils/portal-config.ts
104
110
  function normalizeUrl(url) {
105
111
  return url.replace(/\/$/, "");
106
112
  }
107
- function getDatasetViewUrl(serverUrl, pkg) {
113
+ function getPortalConfig(serverUrl) {
108
114
  const cleanServerUrl = normalizeUrl(serverUrl);
109
115
  const portal = portals_default.portals.find((p) => {
110
116
  const mainUrl = normalizeUrl(p.api_url);
111
117
  const aliases = (p.api_url_aliases || []).map(normalizeUrl);
112
118
  return mainUrl === cleanServerUrl || aliases.includes(cleanServerUrl);
113
119
  });
120
+ return portal || null;
121
+ }
122
+ function getPortalSearchConfig(serverUrl) {
123
+ const portal = getPortalConfig(serverUrl);
124
+ const defaults = portals_default.defaults?.search || {};
125
+ return {
126
+ force_text_field: portal?.search?.force_text_field ?? defaults.force_text_field ?? false
127
+ };
128
+ }
129
+ function normalizePortalUrl(serverUrl) {
130
+ return normalizeUrl(serverUrl);
131
+ }
132
+
133
+ // src/utils/url-generator.ts
134
+ function getDatasetViewUrl(serverUrl, pkg) {
135
+ const cleanServerUrl = normalizePortalUrl(serverUrl);
136
+ const portal = getPortalConfig(serverUrl);
114
137
  const template = portal?.dataset_view_url || portals_default.defaults.dataset_view_url;
115
138
  return template.replace("{server_url}", cleanServerUrl).replace("{id}", pkg.id).replace("{name}", pkg.name);
116
139
  }
117
140
  function getOrganizationViewUrl(serverUrl, org) {
118
- const cleanServerUrl = normalizeUrl(serverUrl);
119
- const portal = portals_default.portals.find((p) => {
120
- const mainUrl = normalizeUrl(p.api_url);
121
- const aliases = (p.api_url_aliases || []).map(normalizeUrl);
122
- return mainUrl === cleanServerUrl || aliases.includes(cleanServerUrl);
123
- });
141
+ const cleanServerUrl = normalizePortalUrl(serverUrl);
142
+ const portal = getPortalConfig(serverUrl);
124
143
  const template = portal?.organization_view_url || portals_default.defaults.organization_view_url;
125
144
  return template.replace("{server_url}", cleanServerUrl).replace("{id}", org.id).replace("{name}", org.name);
126
145
  }
127
146
 
147
+ // src/utils/search.ts
148
+ var DEFAULT_SEARCH_QUERY = "*:*";
149
+ var FIELD_QUERY_PATTERN = /\b[a-zA-Z_][\w-]*:/;
150
+ function isFieldedQuery(query) {
151
+ return FIELD_QUERY_PATTERN.test(query);
152
+ }
153
+ function resolveSearchQuery(serverUrl, query, parserOverride) {
154
+ const portalSearchConfig = getPortalSearchConfig(serverUrl);
155
+ const portalForce = portalSearchConfig.force_text_field ?? false;
156
+ let forceTextField = false;
157
+ if (parserOverride === "text") {
158
+ forceTextField = true;
159
+ } else if (parserOverride === "default") {
160
+ forceTextField = false;
161
+ } else if (portalForce) {
162
+ const trimmedQuery = query.trim();
163
+ forceTextField = trimmedQuery !== DEFAULT_SEARCH_QUERY && !isFieldedQuery(trimmedQuery);
164
+ }
165
+ const effectiveQuery = forceTextField ? `text:(${query})` : query;
166
+ return { effectiveQuery, forcedTextField: forceTextField };
167
+ }
168
+
128
169
  // src/tools/package.ts
129
170
  var DEFAULT_RELEVANCE_WEIGHTS = {
130
171
  title: 4,
@@ -221,6 +262,11 @@ function registerPackageTools(server2) {
221
262
  Supports full Solr search capabilities including filters, facets, and sorting.
222
263
  Use this to discover datasets matching specific criteria.
223
264
 
265
+ Note on parser behavior:
266
+ Some CKAN portals use a restrictive default query parser that can break long OR queries.
267
+ For those portals, this tool may force the query into 'text:(...)' based on per-portal config.
268
+ You can override with 'query_parser' to force or disable this behavior per request.
269
+
224
270
  Args:
225
271
  - server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it/opendata")
226
272
  - q (string): Search query using Solr syntax (default: "*:*" for all)
@@ -231,6 +277,7 @@ Args:
231
277
  - facet_field (array): Fields to facet on (e.g., ["organization", "tags"])
232
278
  - facet_limit (number): Max facet values per field (default: 50)
233
279
  - include_drafts (boolean): Include draft datasets (default: false)
280
+ - query_parser ('default' | 'text'): Override search parser behavior
234
281
  - response_format ('markdown' | 'json'): Output format
235
282
 
236
283
  Returns:
@@ -300,6 +347,7 @@ Examples:
300
347
  facet_field: z2.array(z2.string()).optional().describe("Fields to facet on"),
301
348
  facet_limit: z2.number().int().min(1).optional().default(50).describe("Maximum facet values per field"),
302
349
  include_drafts: z2.boolean().optional().default(false).describe("Include draft datasets"),
350
+ query_parser: z2.enum(["default", "text"]).optional().describe("Override search parser ('text' forces text:(...) on non-fielded queries)"),
303
351
  response_format: ResponseFormatSchema
304
352
  }).strict(),
305
353
  annotations: {
@@ -311,8 +359,13 @@ Examples:
311
359
  },
312
360
  async (params) => {
313
361
  try {
362
+ const { effectiveQuery } = resolveSearchQuery(
363
+ params.server_url,
364
+ params.q,
365
+ params.query_parser
366
+ );
314
367
  const apiParams = {
315
- q: params.q,
368
+ q: effectiveQuery,
316
369
  rows: params.rows,
317
370
  start: params.start,
318
371
  include_private: params.include_drafts
@@ -338,6 +391,8 @@ Examples:
338
391
 
339
392
  **Server**: ${params.server_url}
340
393
  **Query**: ${params.q}
394
+ ${effectiveQuery !== params.q ? `**Effective Query**: ${effectiveQuery}
395
+ ` : ""}
341
396
  ${params.fq ? `**Filter**: ${params.fq}
342
397
  ` : ""}
343
398
  **Total Results**: ${result.count}
@@ -433,6 +488,7 @@ Args:
433
488
  - query (string): Search query text
434
489
  - limit (number): Number of datasets to return (default: 10)
435
490
  - weights (object): Optional weights for title/notes/tags/organization
491
+ - query_parser ('default' | 'text'): Override search parser behavior
436
492
  - response_format ('markdown' | 'json'): Output format
437
493
 
438
494
  Returns:
@@ -451,6 +507,7 @@ Examples:
451
507
  tags: z2.number().min(0).optional(),
452
508
  organization: z2.number().min(0).optional()
453
509
  }).optional().describe("Optional weights per field"),
510
+ query_parser: z2.enum(["default", "text"]).optional().describe("Override search parser ('text' forces text:(...) on non-fielded queries)"),
454
511
  response_format: ResponseFormatSchema
455
512
  }).strict(),
456
513
  annotations: {
@@ -467,11 +524,16 @@ Examples:
467
524
  ...params.weights ?? {}
468
525
  };
469
526
  const rows = Math.min(Math.max(params.limit * 5, params.limit), 100);
527
+ const { effectiveQuery } = resolveSearchQuery(
528
+ params.server_url,
529
+ params.query,
530
+ params.query_parser
531
+ );
470
532
  const searchResult = await makeCkanRequest(
471
533
  params.server_url,
472
534
  "package_search",
473
535
  {
474
- q: params.query,
536
+ q: effectiveQuery,
475
537
  rows,
476
538
  start: 0
477
539
  }
@@ -2087,7 +2149,7 @@ function registerAllResources(server2) {
2087
2149
  function createServer() {
2088
2150
  return new McpServer({
2089
2151
  name: "ckan-mcp-server",
2090
- version: "0.4.5"
2152
+ version: "0.4.7"
2091
2153
  });
2092
2154
  }
2093
2155
  function registerAll(server2) {