@aborruso/ckan-mcp-server 0.4.13 → 0.4.15
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/CLAUDE.md +7 -7
- package/EXAMPLES.md +18 -0
- package/LOG.md +45 -0
- package/PRD.md +3 -3
- package/README.md +19 -8
- package/dist/index.js +191 -20
- package/dist/worker.js +31 -27
- package/examples/langgraph/01_basic_workflow.py +277 -0
- package/examples/langgraph/02_data_exploration.py +366 -0
- package/examples/langgraph/README.md +719 -0
- package/examples/langgraph/metadata_quality.py +299 -0
- package/examples/langgraph/requirements.txt +12 -0
- package/examples/langgraph/setup.sh +32 -0
- package/examples/langgraph/test_setup.py +106 -0
- package/openspec/changes/add-mqa-quality-tool/proposal.md +21 -0
- package/openspec/changes/add-mqa-quality-tool/specs/ckan-quality/spec.md +71 -0
- package/openspec/changes/add-mqa-quality-tool/tasks.md +29 -0
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -41,7 +41,7 @@ The server exposes MCP tools for:
|
|
|
41
41
|
# Build project (uses esbuild - fast and lightweight)
|
|
42
42
|
npm run build
|
|
43
43
|
|
|
44
|
-
# Run test suite (
|
|
44
|
+
# Run test suite (191 tests - unit + integration)
|
|
45
45
|
npm test
|
|
46
46
|
|
|
47
47
|
# Watch mode for tests during development
|
|
@@ -73,7 +73,7 @@ npm run deploy # Deploy to Cloudflare Workers
|
|
|
73
73
|
The project uses **esbuild** for compilation and **vitest** for testing:
|
|
74
74
|
|
|
75
75
|
- **Build**: Ultra-fast builds (milliseconds instead of minutes)
|
|
76
|
-
- **Tests**:
|
|
76
|
+
- **Tests**: 191 tests (unit + integration) with 100% success rate
|
|
77
77
|
- **Coverage**: ~39% overall (utils: 98%, tools: 15-20%) - available via vitest with v8 coverage engine
|
|
78
78
|
|
|
79
79
|
The `build:tsc` script is available as a fallback but can cause memory issues in some environments (particularly WSL). Always use `npm run build` which uses esbuild.
|
|
@@ -88,12 +88,12 @@ The project has a comprehensive test suite using **Vitest**:
|
|
|
88
88
|
tests/
|
|
89
89
|
├── unit/
|
|
90
90
|
│ ├── formatting.test.ts # Utility functions (19 tests)
|
|
91
|
-
│ ├── http.test.ts # HTTP client (
|
|
91
|
+
│ ├── http.test.ts # HTTP client (11 tests)
|
|
92
92
|
│ └── uri.test.ts # URI parsing (11 tests)
|
|
93
93
|
├── integration/
|
|
94
|
-
│ ├── package.test.ts # Package tools (
|
|
94
|
+
│ ├── package.test.ts # Package tools (31 tests)
|
|
95
95
|
│ ├── organization.test.ts # Organization tools (6 tests)
|
|
96
|
-
│ ├── datastore.test.ts # DataStore tools (
|
|
96
|
+
│ ├── datastore.test.ts # DataStore tools (19 tests)
|
|
97
97
|
│ ├── resources.test.ts # MCP Resources (11 tests)
|
|
98
98
|
│ └── status.test.ts # Status tools (2 tests)
|
|
99
99
|
└── fixtures/
|
|
@@ -101,7 +101,7 @@ tests/
|
|
|
101
101
|
└── errors/ # Error scenario mocks
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
**Test Coverage**:
|
|
104
|
+
**Test Coverage**: 191 tests total (117 unit + 74 integration)
|
|
105
105
|
|
|
106
106
|
When making changes:
|
|
107
107
|
1. Run tests before committing: `npm test`
|
|
@@ -341,7 +341,7 @@ To test with Claude Desktop, add the MCP configuration to the config file.
|
|
|
341
341
|
- Direct data access for datasets, resources, organizations
|
|
342
342
|
|
|
343
343
|
**v0.2.0 (2026-01-08)**: Comprehensive test suite
|
|
344
|
-
-
|
|
344
|
+
- 191 tests (unit + integration)
|
|
345
345
|
- ~39% code coverage (utils well-tested, tools improving)
|
|
346
346
|
|
|
347
347
|
**v0.1.0 (2026-01-08)**: Modular refactoring
|
package/EXAMPLES.md
CHANGED
|
@@ -151,6 +151,24 @@ ckan_package_search({
|
|
|
151
151
|
})
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
+
### Get MQA quality metrics for a dataset
|
|
155
|
+
```typescript
|
|
156
|
+
ckan_get_mqa_quality({
|
|
157
|
+
server_url: "https://www.dati.gov.it/opendata",
|
|
158
|
+
dataset_id: "332be8b7-89b9-4dfe-a252-7fccd3efda76",
|
|
159
|
+
response_format: "markdown"
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Returns quality score and detailed metrics from data.europa.eu MQA (Metadata Quality Assurance) system:
|
|
164
|
+
- Overall score (max 405 points)
|
|
165
|
+
- Accessibility (URL status, download availability)
|
|
166
|
+
- Reusability (license, contact point, publisher)
|
|
167
|
+
- Interoperability (format, media type)
|
|
168
|
+
- Findability (keywords, category, spatial/temporal coverage)
|
|
169
|
+
|
|
170
|
+
**Note**: Only works with dati.gov.it datasets. Uses the `identifier` field (or falls back to `name`) to query the European MQA API.
|
|
171
|
+
|
|
154
172
|
## USA Examples - data.gov
|
|
155
173
|
|
|
156
174
|
### Search government datasets
|
package/LOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# LOG
|
|
2
2
|
|
|
3
|
+
## 2026-01-23
|
|
4
|
+
|
|
5
|
+
### MQA Quality Metrics Tool
|
|
6
|
+
|
|
7
|
+
- **Feature**: Added `ckan_get_mqa_quality` tool for retrieving quality metrics from data.europa.eu MQA API
|
|
8
|
+
- **Scope**: Only works with dati.gov.it datasets (server validation enforced)
|
|
9
|
+
- **Data source**: Queries https://data.europa.eu/api/mqa/cache/datasets/{identifier}
|
|
10
|
+
- **Identifier logic**: Uses `identifier` field from CKAN metadata, falls back to `name` if identifier is empty
|
|
11
|
+
- **Metrics returned**:
|
|
12
|
+
- Overall score (max 405 points)
|
|
13
|
+
- Accessibility (URL status, download availability)
|
|
14
|
+
- Reusability (license, contact point, publisher)
|
|
15
|
+
- Interoperability (format, media type)
|
|
16
|
+
- Findability (keywords, category, spatial/temporal coverage)
|
|
17
|
+
- **Output formats**: Markdown (default, human-readable) or JSON (structured data)
|
|
18
|
+
- **Error handling**: Dataset not found, MQA API unavailable, invalid server URL
|
|
19
|
+
- **Tests**: +11 integration tests (212 total, all passing)
|
|
20
|
+
- Server validation (www/non-www dati.gov.it URLs)
|
|
21
|
+
- Quality retrieval with identifier
|
|
22
|
+
- Fallback to name field
|
|
23
|
+
- Error scenarios (404, network errors)
|
|
24
|
+
- Markdown formatting (complete/partial data, availability indicators)
|
|
25
|
+
- **Documentation**: README.md (new Quality Metrics section), EXAMPLES.md (usage example with expected metrics)
|
|
26
|
+
- **Files**:
|
|
27
|
+
- `src/tools/quality.ts` (new, 194 lines)
|
|
28
|
+
- `src/server.ts` (register quality tools)
|
|
29
|
+
- `tests/integration/quality.test.ts` (new, 11 tests)
|
|
30
|
+
- `tests/fixtures/responses/mqa-quality-success.json` (new)
|
|
31
|
+
- `tests/fixtures/responses/package-show-{with,without}-identifier.json` (new)
|
|
32
|
+
- **OpenSpec**: Proposal in `openspec/changes/add-mqa-quality-tool/` (4 requirements, 11 scenarios)
|
|
33
|
+
|
|
34
|
+
## 2026-01-22
|
|
35
|
+
|
|
36
|
+
### Date Query Auto-Conversion (v0.4.14)
|
|
37
|
+
|
|
38
|
+
- **Feature**: Auto-convert NOW-based date math for `modified` and `issued` fields
|
|
39
|
+
- **Problem**: CKAN Solr supports `NOW-XDAYS` syntax only on `metadata_modified` and `metadata_created` fields
|
|
40
|
+
- **Solution**: New `convertDateMathForUnsupportedFields()` automatically converts queries like `modified:[NOW-30DAYS TO NOW]` to ISO dates `modified:[2025-12-23T... TO 2026-01-22T...]`
|
|
41
|
+
- **Supported fields**: `modified`, `issued` (auto-converted) | `metadata_modified`, `metadata_created` (native NOW support)
|
|
42
|
+
- **Supported units**: DAYS, MONTHS, YEARS (singular and plural forms)
|
|
43
|
+
- **Tests**: +10 unit tests (201 total, all passing)
|
|
44
|
+
- **Documentation**: Updated tool description with NOW syntax limitations and examples
|
|
45
|
+
- **Files**: `src/utils/search.ts`, `src/tools/package.ts`, `tests/unit/search.test.ts`
|
|
46
|
+
- **No breaking changes**: Backward compatible - existing queries work unchanged
|
|
47
|
+
|
|
3
48
|
## 2026-01-19
|
|
4
49
|
|
|
5
50
|
### Search Parser Escaping
|
package/PRD.md
CHANGED
|
@@ -214,7 +214,7 @@ An MCP server that exposes tools to interact with CKAN API v3, enabling AI agent
|
|
|
214
214
|
- `wrangler@^4.58.0` - Cloudflare Workers CLI
|
|
215
215
|
|
|
216
216
|
**Test Framework**:
|
|
217
|
-
- `vitest@^4.0.16` - Test runner (
|
|
217
|
+
- `vitest@^4.0.16` - Test runner (191 tests, 100% passing)
|
|
218
218
|
|
|
219
219
|
### 4.2 Architecture Diagram
|
|
220
220
|
|
|
@@ -825,7 +825,7 @@ ckan_package_search({
|
|
|
825
825
|
### 10.3 Testing & Quality
|
|
826
826
|
|
|
827
827
|
✅ **Current State**:
|
|
828
|
-
-
|
|
828
|
+
- 191 unit and integration tests (100% passing)
|
|
829
829
|
- vitest test runner
|
|
830
830
|
- Coverage for all 13 tools
|
|
831
831
|
- Fixtures for offline testing
|
|
@@ -860,7 +860,7 @@ ckan_package_search({
|
|
|
860
860
|
- **Memory Usage**: < 50MB runtime (Node.js), Workers limits apply
|
|
861
861
|
- **Response Time**: < 30s (CKAN API timeout), < 10s (Workers)
|
|
862
862
|
- **Cold Start**: < 60ms (Cloudflare Workers)
|
|
863
|
-
- **Test Coverage**:
|
|
863
|
+
- **Test Coverage**: 191 tests (100% passing)
|
|
864
864
|
|
|
865
865
|
### 11.2 Distribution Metrics
|
|
866
866
|
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ MCP (Model Context Protocol) server for interacting with CKAN-based open data po
|
|
|
17
17
|
- ⚡ Pagination and faceting support
|
|
18
18
|
- 📄 MCP Resource Templates for direct data access
|
|
19
19
|
- 🧭 Guided MCP prompts for common workflows
|
|
20
|
-
- 🧪 Test suite with
|
|
20
|
+
- 🧪 Test suite with 212 tests (100% passing)
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -45,7 +45,7 @@ npm install
|
|
|
45
45
|
# Build with esbuild (fast, ~4ms)
|
|
46
46
|
npm run build
|
|
47
47
|
|
|
48
|
-
# Run tests (
|
|
48
|
+
# Run tests (212 tests)
|
|
49
49
|
npm test
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -117,22 +117,29 @@ Then add to `claude_desktop_config.json`:
|
|
|
117
117
|
}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
#### Option 2: Local Installation (Optional)
|
|
120
|
+
#### Option 2: Local Project Installation (Optional)
|
|
121
121
|
|
|
122
|
-
If you
|
|
122
|
+
If you want to install the server in a specific project:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cd your-project
|
|
126
|
+
npm install @aborruso/ckan-mcp-server
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then add to `claude_desktop_config.json`:
|
|
123
130
|
|
|
124
131
|
```json
|
|
125
132
|
{
|
|
126
133
|
"mcpServers": {
|
|
127
134
|
"ckan": {
|
|
128
|
-
"command": "
|
|
129
|
-
"args": ["/
|
|
135
|
+
"command": "npx",
|
|
136
|
+
"args": ["@aborruso/ckan-mcp-server"]
|
|
130
137
|
}
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
```
|
|
134
141
|
|
|
135
|
-
|
|
142
|
+
**Note**: `npx` will use the locally installed package in `node_modules`. Make sure to install the package first.
|
|
136
143
|
|
|
137
144
|
#### Option 3: From Source
|
|
138
145
|
|
|
@@ -202,6 +209,10 @@ These guides are based on a public demo server, which has a limit of 100,000 cal
|
|
|
202
209
|
- **ckan_group_show**: Show group details
|
|
203
210
|
- **ckan_group_search**: Search groups by name
|
|
204
211
|
|
|
212
|
+
### Quality Metrics
|
|
213
|
+
|
|
214
|
+
- **ckan_get_mqa_quality**: Get MQA quality score and metrics for dati.gov.it datasets (accessibility, reusability, interoperability, findability)
|
|
215
|
+
|
|
205
216
|
### Utilities
|
|
206
217
|
|
|
207
218
|
- **ckan_status_show**: Verify server status
|
|
@@ -559,7 +570,7 @@ ckan-mcp-server/
|
|
|
559
570
|
│ └── transport/
|
|
560
571
|
│ ├── stdio.ts # Stdio transport
|
|
561
572
|
│ └── http.ts # HTTP transport
|
|
562
|
-
├── tests/ # Test suite (
|
|
573
|
+
├── tests/ # Test suite (212 tests)
|
|
563
574
|
├── dist/ # Compiled files (generated)
|
|
564
575
|
├── package.json
|
|
565
576
|
└── README.md
|
package/dist/index.js
CHANGED
|
@@ -168,6 +168,31 @@ function isFieldedQuery(query) {
|
|
|
168
168
|
function escapeSolrQuery(query) {
|
|
169
169
|
return query.replace(SOLR_SPECIAL_CHARS, "\\$&");
|
|
170
170
|
}
|
|
171
|
+
function convertDateMathForUnsupportedFields(query) {
|
|
172
|
+
const now = /* @__PURE__ */ new Date();
|
|
173
|
+
const nowIso = now.toISOString();
|
|
174
|
+
const pattern = /\b(?!metadata_)(modified|issued):\[NOW-(\d+)(DAYS?|MONTHS?|YEARS?)\s+TO\s+NOW\]/gi;
|
|
175
|
+
return query.replace(pattern, (match, field, amount, unit) => {
|
|
176
|
+
const amountNum = parseInt(amount, 10);
|
|
177
|
+
const startDate = new Date(now);
|
|
178
|
+
const normalizedUnit = unit.toLowerCase().replace(/s$/, "");
|
|
179
|
+
switch (normalizedUnit) {
|
|
180
|
+
case "day":
|
|
181
|
+
startDate.setDate(startDate.getDate() - amountNum);
|
|
182
|
+
break;
|
|
183
|
+
case "month":
|
|
184
|
+
startDate.setMonth(startDate.getMonth() - amountNum);
|
|
185
|
+
break;
|
|
186
|
+
case "year":
|
|
187
|
+
startDate.setFullYear(startDate.getFullYear() - amountNum);
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
return match;
|
|
191
|
+
}
|
|
192
|
+
const startIso = startDate.toISOString();
|
|
193
|
+
return `${field}:[${startIso} TO ${nowIso}]`;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
171
196
|
function resolveSearchQuery(serverUrl, query, parserOverride) {
|
|
172
197
|
const portalSearchConfig = getPortalSearchConfig(serverUrl);
|
|
173
198
|
const portalForce = portalSearchConfig.force_text_field ?? false;
|
|
@@ -180,7 +205,8 @@ function resolveSearchQuery(serverUrl, query, parserOverride) {
|
|
|
180
205
|
const trimmedQuery = query.trim();
|
|
181
206
|
forceTextField = trimmedQuery !== DEFAULT_SEARCH_QUERY && !isFieldedQuery(trimmedQuery);
|
|
182
207
|
}
|
|
183
|
-
|
|
208
|
+
let effectiveQuery = forceTextField ? `text:(${escapeSolrQuery(query)})` : query;
|
|
209
|
+
effectiveQuery = convertDateMathForUnsupportedFields(effectiveQuery);
|
|
184
210
|
return { effectiveQuery, forcedTextField: forceTextField };
|
|
185
211
|
}
|
|
186
212
|
|
|
@@ -333,6 +359,9 @@ Query Syntax (parameter q):
|
|
|
333
359
|
- NOW/DAY, NOW/MONTH (round down)
|
|
334
360
|
- Combined: "metadata_modified:[NOW-2MONTHS TO NOW]"
|
|
335
361
|
- Example: "metadata_created:[NOW-1YEAR TO *]"
|
|
362
|
+
- IMPORTANT: NOW syntax works on metadata_modified and metadata_created fields
|
|
363
|
+
- For 'modified' and 'issued' fields, NOW syntax is auto-converted to ISO dates
|
|
364
|
+
- Manual ISO dates always work: "modified:[2026-01-15T00:00:00Z TO *]"
|
|
336
365
|
|
|
337
366
|
Field existence:
|
|
338
367
|
- Exists: "field:*" or "field:[* TO *]"
|
|
@@ -351,6 +380,7 @@ Examples:
|
|
|
351
380
|
- Proximity: { q: "notes:"open data"~3" }
|
|
352
381
|
- Date range: { q: "metadata_modified:[2024-01-01T00:00:00Z TO 2024-12-31T23:59:59Z]" }
|
|
353
382
|
- Date math: { q: "metadata_modified:[NOW-6MONTHS TO *]" }
|
|
383
|
+
- Date math (auto-converted): { q: "modified:[NOW-30DAYS TO NOW]" }
|
|
354
384
|
- Field exists: { q: "organization:* AND num_resources:[1 TO *]" }
|
|
355
385
|
- Boosting: { q: "title:climate^2 OR notes:climate" }
|
|
356
386
|
- Filter org: { fq: "organization:regione-siciliana" }
|
|
@@ -2071,6 +2101,146 @@ Returns:
|
|
|
2071
2101
|
);
|
|
2072
2102
|
}
|
|
2073
2103
|
|
|
2104
|
+
// src/tools/quality.ts
|
|
2105
|
+
import { z as z8 } from "zod";
|
|
2106
|
+
import axios2 from "axios";
|
|
2107
|
+
var MQA_API_BASE = "https://data.europa.eu/api/mqa/cache/datasets";
|
|
2108
|
+
var ALLOWED_SERVER_PATTERNS = [
|
|
2109
|
+
/^https?:\/\/(www\.)?dati\.gov\.it/i
|
|
2110
|
+
];
|
|
2111
|
+
function isValidMqaServer(serverUrl) {
|
|
2112
|
+
return ALLOWED_SERVER_PATTERNS.some((pattern) => pattern.test(serverUrl));
|
|
2113
|
+
}
|
|
2114
|
+
async function getMqaQuality(serverUrl, datasetId) {
|
|
2115
|
+
const dataset = await makeCkanRequest(
|
|
2116
|
+
serverUrl,
|
|
2117
|
+
"package_show",
|
|
2118
|
+
{ id: datasetId }
|
|
2119
|
+
);
|
|
2120
|
+
const europeanId = dataset.identifier || dataset.name;
|
|
2121
|
+
const mqaUrl = `${MQA_API_BASE}/${europeanId}`;
|
|
2122
|
+
try {
|
|
2123
|
+
const response = await axios2.get(mqaUrl, {
|
|
2124
|
+
timeout: 3e4,
|
|
2125
|
+
headers: {
|
|
2126
|
+
"User-Agent": "CKAN-MCP-Server/1.0"
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
return response.data;
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
if (axios2.isAxiosError(error)) {
|
|
2132
|
+
if (error.response?.status === 404) {
|
|
2133
|
+
throw new Error(`Quality metrics not found for dataset '${europeanId}' on data.europa.eu`);
|
|
2134
|
+
}
|
|
2135
|
+
throw new Error(`MQA API error: ${error.message}`);
|
|
2136
|
+
}
|
|
2137
|
+
throw error;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
function formatQualityMarkdown(data, datasetId) {
|
|
2141
|
+
const lines = [];
|
|
2142
|
+
lines.push(`# Quality Metrics for Dataset: ${datasetId}`);
|
|
2143
|
+
lines.push("");
|
|
2144
|
+
if (data.info?.score !== void 0) {
|
|
2145
|
+
lines.push(`**Overall Score**: ${data.info.score}/405`);
|
|
2146
|
+
lines.push("");
|
|
2147
|
+
}
|
|
2148
|
+
if (data.accessibility) {
|
|
2149
|
+
lines.push("## Accessibility");
|
|
2150
|
+
if (data.accessibility.accessUrl !== void 0) {
|
|
2151
|
+
lines.push(`- Access URL: ${data.accessibility.accessUrl.available ? "\u2713" : "\u2717"} Available`);
|
|
2152
|
+
}
|
|
2153
|
+
if (data.accessibility.downloadUrl !== void 0) {
|
|
2154
|
+
lines.push(`- Download URL: ${data.accessibility.downloadUrl.available ? "\u2713" : "\u2717"} Available`);
|
|
2155
|
+
}
|
|
2156
|
+
lines.push("");
|
|
2157
|
+
}
|
|
2158
|
+
if (data.reusability) {
|
|
2159
|
+
lines.push("## Reusability");
|
|
2160
|
+
if (data.reusability.licence !== void 0) {
|
|
2161
|
+
lines.push(`- License: ${data.reusability.licence.available ? "\u2713" : "\u2717"} Available`);
|
|
2162
|
+
}
|
|
2163
|
+
if (data.reusability.contactPoint !== void 0) {
|
|
2164
|
+
lines.push(`- Contact Point: ${data.reusability.contactPoint.available ? "\u2713" : "\u2717"} Available`);
|
|
2165
|
+
}
|
|
2166
|
+
if (data.reusability.publisher !== void 0) {
|
|
2167
|
+
lines.push(`- Publisher: ${data.reusability.publisher.available ? "\u2713" : "\u2717"} Available`);
|
|
2168
|
+
}
|
|
2169
|
+
lines.push("");
|
|
2170
|
+
}
|
|
2171
|
+
if (data.interoperability) {
|
|
2172
|
+
lines.push("## Interoperability");
|
|
2173
|
+
if (data.interoperability.format !== void 0) {
|
|
2174
|
+
lines.push(`- Format: ${data.interoperability.format.available ? "\u2713" : "\u2717"} Available`);
|
|
2175
|
+
}
|
|
2176
|
+
if (data.interoperability.mediaType !== void 0) {
|
|
2177
|
+
lines.push(`- Media Type: ${data.interoperability.mediaType.available ? "\u2713" : "\u2717"} Available`);
|
|
2178
|
+
}
|
|
2179
|
+
lines.push("");
|
|
2180
|
+
}
|
|
2181
|
+
if (data.findability) {
|
|
2182
|
+
lines.push("## Findability");
|
|
2183
|
+
if (data.findability.keyword !== void 0) {
|
|
2184
|
+
lines.push(`- Keywords: ${data.findability.keyword.available ? "\u2713" : "\u2717"} Available`);
|
|
2185
|
+
}
|
|
2186
|
+
if (data.findability.category !== void 0) {
|
|
2187
|
+
lines.push(`- Category: ${data.findability.category.available ? "\u2713" : "\u2717"} Available`);
|
|
2188
|
+
}
|
|
2189
|
+
if (data.findability.spatial !== void 0) {
|
|
2190
|
+
lines.push(`- Spatial: ${data.findability.spatial.available ? "\u2713" : "\u2717"} Available`);
|
|
2191
|
+
}
|
|
2192
|
+
if (data.findability.temporal !== void 0) {
|
|
2193
|
+
lines.push(`- Temporal: ${data.findability.temporal.available ? "\u2713" : "\u2717"} Available`);
|
|
2194
|
+
}
|
|
2195
|
+
lines.push("");
|
|
2196
|
+
}
|
|
2197
|
+
lines.push("---");
|
|
2198
|
+
lines.push(`Source: ${MQA_API_BASE}/${data.id || datasetId}`);
|
|
2199
|
+
return lines.join("\n");
|
|
2200
|
+
}
|
|
2201
|
+
function registerQualityTools(server2) {
|
|
2202
|
+
server2.tool(
|
|
2203
|
+
"ckan_get_mqa_quality",
|
|
2204
|
+
"Get MQA (Metadata Quality Assurance) quality metrics for a dataset on dati.gov.it. Returns quality score and detailed metrics (accessibility, reusability, interoperability, findability) from data.europa.eu. Only works with dati.gov.it server.",
|
|
2205
|
+
{
|
|
2206
|
+
server_url: z8.string().url().describe("Base URL of dati.gov.it (e.g., https://www.dati.gov.it/opendata)"),
|
|
2207
|
+
dataset_id: z8.string().describe("Dataset ID or name"),
|
|
2208
|
+
response_format: ResponseFormatSchema.optional()
|
|
2209
|
+
},
|
|
2210
|
+
async ({ server_url, dataset_id, response_format }) => {
|
|
2211
|
+
if (!isValidMqaServer(server_url)) {
|
|
2212
|
+
return {
|
|
2213
|
+
content: [{
|
|
2214
|
+
type: "text",
|
|
2215
|
+
text: `Error: MQA quality metrics are only available for dati.gov.it datasets. Provided server: ${server_url}
|
|
2216
|
+
|
|
2217
|
+
The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and only evaluates datasets from Italian open data portal.`
|
|
2218
|
+
}]
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
try {
|
|
2222
|
+
const qualityData = await getMqaQuality(server_url, dataset_id);
|
|
2223
|
+
const format = response_format || "markdown" /* MARKDOWN */;
|
|
2224
|
+
const output = format === "json" /* JSON */ ? JSON.stringify(qualityData, null, 2) : formatQualityMarkdown(qualityData, dataset_id);
|
|
2225
|
+
return {
|
|
2226
|
+
content: [{
|
|
2227
|
+
type: "text",
|
|
2228
|
+
text: output
|
|
2229
|
+
}]
|
|
2230
|
+
};
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2233
|
+
return {
|
|
2234
|
+
content: [{
|
|
2235
|
+
type: "text",
|
|
2236
|
+
text: `Error retrieving quality metrics: ${errorMessage}`
|
|
2237
|
+
}]
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2074
2244
|
// src/resources/dataset.ts
|
|
2075
2245
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2076
2246
|
|
|
@@ -2340,7 +2510,7 @@ function registerAllResources(server2) {
|
|
|
2340
2510
|
}
|
|
2341
2511
|
|
|
2342
2512
|
// src/prompts/theme.ts
|
|
2343
|
-
import { z as
|
|
2513
|
+
import { z as z9 } from "zod";
|
|
2344
2514
|
|
|
2345
2515
|
// src/prompts/types.ts
|
|
2346
2516
|
var createTextPrompt = (text) => ({
|
|
@@ -2397,9 +2567,9 @@ var registerThemePrompt = (server2) => {
|
|
|
2397
2567
|
title: "Search datasets by theme",
|
|
2398
2568
|
description: "Guided prompt to discover a theme and search datasets under it.",
|
|
2399
2569
|
argsSchema: {
|
|
2400
|
-
server_url:
|
|
2401
|
-
theme:
|
|
2402
|
-
rows:
|
|
2570
|
+
server_url: z9.string().url().describe("Base URL of the CKAN server"),
|
|
2571
|
+
theme: z9.string().min(1).describe("Theme or group name to search"),
|
|
2572
|
+
rows: z9.coerce.number().int().positive().default(10).describe("Max results to return")
|
|
2403
2573
|
}
|
|
2404
2574
|
},
|
|
2405
2575
|
async ({ server_url, theme, rows }) => createTextPrompt(buildThemePromptText(server_url, theme, rows))
|
|
@@ -2407,7 +2577,7 @@ var registerThemePrompt = (server2) => {
|
|
|
2407
2577
|
};
|
|
2408
2578
|
|
|
2409
2579
|
// src/prompts/organization.ts
|
|
2410
|
-
import { z as
|
|
2580
|
+
import { z as z10 } from "zod";
|
|
2411
2581
|
var ORGANIZATION_PROMPT_NAME = "ckan-search-by-organization";
|
|
2412
2582
|
var buildOrganizationPromptText = (serverUrl, organization, rows) => `# Guided search: datasets by organization
|
|
2413
2583
|
|
|
@@ -2438,9 +2608,9 @@ var registerOrganizationPrompt = (server2) => {
|
|
|
2438
2608
|
title: "Search datasets by organization",
|
|
2439
2609
|
description: "Guided prompt to find a publisher and list its datasets.",
|
|
2440
2610
|
argsSchema: {
|
|
2441
|
-
server_url:
|
|
2442
|
-
organization:
|
|
2443
|
-
rows:
|
|
2611
|
+
server_url: z10.string().url().describe("Base URL of the CKAN server"),
|
|
2612
|
+
organization: z10.string().min(1).describe("Organization name or keyword"),
|
|
2613
|
+
rows: z10.coerce.number().int().positive().default(10).describe("Max results to return")
|
|
2444
2614
|
}
|
|
2445
2615
|
},
|
|
2446
2616
|
async ({ server_url, organization, rows }) => createTextPrompt(buildOrganizationPromptText(server_url, organization, rows))
|
|
@@ -2448,7 +2618,7 @@ var registerOrganizationPrompt = (server2) => {
|
|
|
2448
2618
|
};
|
|
2449
2619
|
|
|
2450
2620
|
// src/prompts/format.ts
|
|
2451
|
-
import { z as
|
|
2621
|
+
import { z as z11 } from "zod";
|
|
2452
2622
|
var FORMAT_PROMPT_NAME = "ckan-search-by-format";
|
|
2453
2623
|
var buildFormatPromptText = (serverUrl, format, rows) => `# Guided search: datasets by resource format
|
|
2454
2624
|
|
|
@@ -2469,9 +2639,9 @@ var registerFormatPrompt = (server2) => {
|
|
|
2469
2639
|
title: "Search datasets by resource format",
|
|
2470
2640
|
description: "Guided prompt to find datasets with a given resource format.",
|
|
2471
2641
|
argsSchema: {
|
|
2472
|
-
server_url:
|
|
2473
|
-
format:
|
|
2474
|
-
rows:
|
|
2642
|
+
server_url: z11.string().url().describe("Base URL of the CKAN server"),
|
|
2643
|
+
format: z11.string().min(1).describe("Resource format (e.g., CSV, JSON)"),
|
|
2644
|
+
rows: z11.coerce.number().int().positive().default(10).describe("Max results to return")
|
|
2475
2645
|
}
|
|
2476
2646
|
},
|
|
2477
2647
|
async ({ server_url, format, rows }) => createTextPrompt(buildFormatPromptText(server_url, format, rows))
|
|
@@ -2479,7 +2649,7 @@ var registerFormatPrompt = (server2) => {
|
|
|
2479
2649
|
};
|
|
2480
2650
|
|
|
2481
2651
|
// src/prompts/recent.ts
|
|
2482
|
-
import { z as
|
|
2652
|
+
import { z as z12 } from "zod";
|
|
2483
2653
|
var RECENT_PROMPT_NAME = "ckan-recent-datasets";
|
|
2484
2654
|
var buildRecentPromptText = (serverUrl, rows) => `# Guided search: recent datasets
|
|
2485
2655
|
|
|
@@ -2507,8 +2677,8 @@ var registerRecentPrompt = (server2) => {
|
|
|
2507
2677
|
title: "Find recently updated datasets",
|
|
2508
2678
|
description: "Guided prompt to list recently updated datasets on a CKAN portal.",
|
|
2509
2679
|
argsSchema: {
|
|
2510
|
-
server_url:
|
|
2511
|
-
rows:
|
|
2680
|
+
server_url: z12.string().url().describe("Base URL of the CKAN server"),
|
|
2681
|
+
rows: z12.coerce.number().int().positive().default(10).describe("Max results to return")
|
|
2512
2682
|
}
|
|
2513
2683
|
},
|
|
2514
2684
|
async ({ server_url, rows }) => createTextPrompt(buildRecentPromptText(server_url, rows))
|
|
@@ -2516,7 +2686,7 @@ var registerRecentPrompt = (server2) => {
|
|
|
2516
2686
|
};
|
|
2517
2687
|
|
|
2518
2688
|
// src/prompts/dataset-analysis.ts
|
|
2519
|
-
import { z as
|
|
2689
|
+
import { z as z13 } from "zod";
|
|
2520
2690
|
var DATASET_ANALYSIS_PROMPT_NAME = "ckan-analyze-dataset";
|
|
2521
2691
|
var buildDatasetAnalysisPromptText = (serverUrl, id) => `# Guided analysis: dataset
|
|
2522
2692
|
|
|
@@ -2558,8 +2728,8 @@ var registerDatasetAnalysisPrompt = (server2) => {
|
|
|
2558
2728
|
title: "Analyze a dataset",
|
|
2559
2729
|
description: "Guided prompt to inspect dataset metadata and explore DataStore tables.",
|
|
2560
2730
|
argsSchema: {
|
|
2561
|
-
server_url:
|
|
2562
|
-
id:
|
|
2731
|
+
server_url: z13.string().url().describe("Base URL of the CKAN server"),
|
|
2732
|
+
id: z13.string().min(1).describe("Dataset id or name (CKAN package id)")
|
|
2563
2733
|
}
|
|
2564
2734
|
},
|
|
2565
2735
|
async ({ server_url, id }) => createTextPrompt(buildDatasetAnalysisPromptText(server_url, id))
|
|
@@ -2579,7 +2749,7 @@ var registerAllPrompts = (server2) => {
|
|
|
2579
2749
|
function createServer() {
|
|
2580
2750
|
return new McpServer({
|
|
2581
2751
|
name: "ckan-mcp-server",
|
|
2582
|
-
version: "0.4.
|
|
2752
|
+
version: "0.4.15"
|
|
2583
2753
|
});
|
|
2584
2754
|
}
|
|
2585
2755
|
function registerAll(server2) {
|
|
@@ -2589,6 +2759,7 @@ function registerAll(server2) {
|
|
|
2589
2759
|
registerStatusTools(server2);
|
|
2590
2760
|
registerTagTools(server2);
|
|
2591
2761
|
registerGroupTools(server2);
|
|
2762
|
+
registerQualityTools(server2);
|
|
2592
2763
|
registerAllResources(server2);
|
|
2593
2764
|
registerAllPrompts(server2);
|
|
2594
2765
|
}
|