@aborruso/ckan-mcp-server 0.4.49 → 0.4.50
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/AGENTS.md +37 -0
- package/LOG.md +9 -0
- package/README.md +20 -1
- package/dist/index.js +92 -13
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -118,6 +118,43 @@ Node `>=18`. Worker build in `wrangler.toml`. Vitest coverage thresholds enforce
|
|
|
118
118
|
|
|
119
119
|
Minimal focused diffs. No unrelated refactors. Update tests for behavior changes. Avoid editing `dist/`.
|
|
120
120
|
|
|
121
|
+
## Pre-commit Checklist
|
|
122
|
+
|
|
123
|
+
Before committing and pushing any locally testable change:
|
|
124
|
+
1. Build: `npm run build`
|
|
125
|
+
2. Automated tests: `npm test` — all must pass
|
|
126
|
+
3. Manual queries: run real requests against the built server to verify end-to-end behavior
|
|
127
|
+
|
|
128
|
+
### How to run manual queries
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Terminal 1 — start server
|
|
132
|
+
TRANSPORT=http PORT=3001 node dist/index.js
|
|
133
|
+
|
|
134
|
+
# Terminal 2 — call a tool
|
|
135
|
+
curl -s -X POST http://localhost:3001/mcp \
|
|
136
|
+
-H "Content-Type: application/json" \
|
|
137
|
+
-H "Accept: application/json, text/event-stream" \
|
|
138
|
+
-d '{
|
|
139
|
+
"jsonrpc":"2.0",
|
|
140
|
+
"method":"tools/call",
|
|
141
|
+
"params":{
|
|
142
|
+
"name":"ckan_package_search",
|
|
143
|
+
"arguments":{
|
|
144
|
+
"server_url":"https://www.dati.gov.it/opendata",
|
|
145
|
+
"q":"ambiente",
|
|
146
|
+
"page":1,
|
|
147
|
+
"page_size":3
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"id":1
|
|
151
|
+
}'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- Always include both `Content-Type: application/json` and `Accept: application/json, text/event-stream`
|
|
155
|
+
- Use `node dist/index.js` directly, not `npm start`
|
|
156
|
+
- Use port 3001 to avoid conflicts
|
|
157
|
+
|
|
121
158
|
## Project Layout
|
|
122
159
|
|
|
123
160
|
`src/index.ts` entry, `src/server.ts` wiring, `src/tools/` handlers, `src/utils/` helpers, `src/resources/` templates, `src/transport/` stdio/HTTP. `tests/unit/` utilities, `tests/integration/` behavior, `tests/fixtures/` mocks.
|
package/LOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# LOG
|
|
2
2
|
|
|
3
|
+
## 2026-02-26 (v0.4.50)
|
|
4
|
+
|
|
5
|
+
- `ckan_list_resources`: add `format_filter` param (case-insensitive, client-side) — e.g. 72 resources → 8 CSV; header shows "Total: 72 (showing 8 CSV)"
|
|
6
|
+
- `ckan_package_search`: OR tip on zero results — when a plain multi-term query returns 0, suggest the OR version (e.g. `"a b c"` → `"a OR b OR c"`)
|
|
7
|
+
- `ckan_package_search`: accent fallback — if query returns 0 results and contains accented chars, retry with accent-stripped query; note shown in output
|
|
8
|
+
- `ckan_package_show`: always show DataStore status per resource
|
|
9
|
+
- `✅ Available` / `❌ Not available` / `❓ Not reported by portal`
|
|
10
|
+
- Previously silent when field absent (e.g. dati.gov.it); now explicit
|
|
11
|
+
|
|
3
12
|
## 2026-02-25 (v0.4.49)
|
|
4
13
|
|
|
5
14
|
- Disable DataStore Table UI component (MCP Apps) pending use-case design
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ MCP (Model Context Protocol) server for interacting with CKAN-based open data po
|
|
|
18
18
|
- 📄 MCP Resource Templates for direct data access
|
|
19
19
|
- 🧭 Guided MCP prompts for common workflows
|
|
20
20
|
- 🛡️ Browser-like headers to avoid WAF blocks
|
|
21
|
-
- 🧪
|
|
21
|
+
- 🧪 Comprehensive test suite (100% passing)
|
|
22
22
|
|
|
23
23
|
👉 If you want to dive deeper, the [**AI-generated DeepWiki**](https://deepwiki.com/ondata/ckan-mcp-server) is very well done.
|
|
24
24
|
|
|
@@ -188,6 +188,25 @@ npm install -g @aborruso/ckan-mcp-server
|
|
|
188
188
|
|
|
189
189
|
⚠️ **Warning**: Demo instance with 100,000 requests/month shared globally across all users. Not reliable for production use.
|
|
190
190
|
|
|
191
|
+
**Claude Desktop on Windows reading from a local MCP server installed on WSL2**:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"mcpServers": {
|
|
196
|
+
"ckan": {
|
|
197
|
+
"command": "wsl.exe",
|
|
198
|
+
"args": [
|
|
199
|
+
"-e",
|
|
200
|
+
"/usr/local/bin/node",
|
|
201
|
+
"/home/username/projects/ckan-mcp-server/dist/index.js"
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
This requires the server to be built (`npm run build`) inside the WSL2 environment before use.
|
|
209
|
+
|
|
191
210
|
### Web Tools
|
|
192
211
|
|
|
193
212
|
#### ChatGPT
|
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
|
|
765
|
-
markdown += `- **DataStore**:
|
|
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:
|
|
934
|
-
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
|
-
|
|
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 ${
|
|
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 >
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
|
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:
|
|
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**: ${
|
|
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) {
|
|
@@ -3896,7 +3975,7 @@ var registerAllPrompts = (server2) => {
|
|
|
3896
3975
|
function createServer() {
|
|
3897
3976
|
return new McpServer({
|
|
3898
3977
|
name: "ckan-mcp-server",
|
|
3899
|
-
version: "0.4.
|
|
3978
|
+
version: "0.4.50"
|
|
3900
3979
|
});
|
|
3901
3980
|
}
|
|
3902
3981
|
function registerAll(server2) {
|