@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 +10 -0
- package/LOG.md +6 -0
- package/README.md +35 -36
- package/dist/index.js +74 -12
- package/dist/worker.js +130 -74
- package/openspec/changes/update-search-parser-config/proposal.md +13 -0
- package/openspec/changes/update-search-parser-config/specs/ckan-insights/spec.md +11 -0
- package/openspec/changes/update-search-parser-config/specs/ckan-search/spec.md +11 -0
- package/openspec/changes/update-search-parser-config/tasks.md +6 -0
- package/openspec/project.md +9 -7
- package/openspec/specs/ckan-insights/spec.md +8 -1
- package/package.json +1 -1
- /package/openspec/changes/{add-ckan-find-relevant-datasets → archive/2026-01-10-add-ckan-find-relevant-datasets}/proposal.md +0 -0
- /package/openspec/changes/{add-ckan-find-relevant-datasets → archive/2026-01-10-add-ckan-find-relevant-datasets}/specs/ckan-insights/spec.md +0 -0
- /package/openspec/changes/{add-ckan-find-relevant-datasets → archive/2026-01-10-add-ckan-find-relevant-datasets}/tasks.md +0 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
481
|
-
│ ├── server.ts
|
|
482
|
-
│ ├──
|
|
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
|
|
485
|
-
│ │
|
|
480
|
+
│ │ ├── http.ts # CKAN API client
|
|
481
|
+
│ │ ├── formatting.ts # Output formatting
|
|
482
|
+
│ │ └── url-generator.ts
|
|
486
483
|
│ ├── tools/
|
|
487
|
-
│ │ ├── package.ts
|
|
484
|
+
│ │ ├── package.ts # Package search/show
|
|
488
485
|
│ │ ├── organization.ts # Organization tools
|
|
489
|
-
│ │ ├── datastore.ts
|
|
490
|
-
│ │
|
|
491
|
-
│ ├──
|
|
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
|
|
492
|
+
│ │ ├── uri.ts # URI parsing
|
|
494
493
|
│ │ ├── dataset.ts
|
|
495
494
|
│ │ ├── resource.ts
|
|
496
495
|
│ │ └── organization.ts
|
|
497
496
|
│ └── transport/
|
|
498
|
-
│ ├── stdio.ts
|
|
499
|
-
│ └── http.ts
|
|
500
|
-
├── tests/
|
|
501
|
-
├── dist/
|
|
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/
|
|
109
|
+
// src/utils/portal-config.ts
|
|
104
110
|
function normalizeUrl(url) {
|
|
105
111
|
return url.replace(/\/$/, "");
|
|
106
112
|
}
|
|
107
|
-
function
|
|
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 =
|
|
119
|
-
const portal =
|
|
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:
|
|
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:
|
|
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.
|
|
2152
|
+
version: "0.4.7"
|
|
2091
2153
|
});
|
|
2092
2154
|
}
|
|
2093
2155
|
function registerAll(server2) {
|