@aborruso/ckan-mcp-server 0.3.1
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/commands/openspec/apply.md +23 -0
- package/.claude/commands/openspec/archive.md +27 -0
- package/.claude/commands/openspec/proposal.md +28 -0
- package/.claude/settings.local.json +31 -0
- package/.gemini/commands/openspec/apply.toml +21 -0
- package/.gemini/commands/openspec/archive.toml +25 -0
- package/.gemini/commands/openspec/proposal.toml +26 -0
- package/.mcp.json +12 -0
- package/.opencode/command/openspec-apply.md +24 -0
- package/.opencode/command/openspec-archive.md +27 -0
- package/.opencode/command/openspec-proposal.md +29 -0
- package/AGENTS.md +18 -0
- package/CLAUDE.md +320 -0
- package/EXAMPLES.md +707 -0
- package/LICENSE.txt +21 -0
- package/LOG.md +154 -0
- package/PRD.md +912 -0
- package/README.md +468 -0
- package/REFACTORING.md +237 -0
- package/dist/index.js +1277 -0
- package/openspec/AGENTS.md +456 -0
- package/openspec/changes/archive/2026-01-08-add-mcp-resources/design.md +115 -0
- package/openspec/changes/archive/2026-01-08-add-mcp-resources/proposal.md +52 -0
- package/openspec/changes/archive/2026-01-08-add-mcp-resources/specs/mcp-resources/spec.md +92 -0
- package/openspec/changes/archive/2026-01-08-add-mcp-resources/tasks.md +56 -0
- package/openspec/changes/archive/2026-01-08-expand-test-coverage-specs/design.md +355 -0
- package/openspec/changes/archive/2026-01-08-expand-test-coverage-specs/proposal.md +161 -0
- package/openspec/changes/archive/2026-01-08-expand-test-coverage-specs/tasks.md +162 -0
- package/openspec/changes/archive/2026-01-08-translate-project-to-english/proposal.md +115 -0
- package/openspec/changes/archive/2026-01-08-translate-project-to-english/specs/documentation-language/spec.md +32 -0
- package/openspec/changes/archive/2026-01-08-translate-project-to-english/tasks.md +115 -0
- package/openspec/changes/archive/add-automated-tests/design.md +324 -0
- package/openspec/changes/archive/add-automated-tests/proposal.md +167 -0
- package/openspec/changes/archive/add-automated-tests/specs/automated-testing/spec.md +143 -0
- package/openspec/changes/archive/add-automated-tests/tasks.md +132 -0
- package/openspec/project.md +113 -0
- package/openspec/specs/documentation-language/spec.md +32 -0
- package/openspec/specs/mcp-resources/spec.md +94 -0
- package/package.json +46 -0
- package/spunti.md +19 -0
- package/tasks/todo.md +124 -0
- package/test-urls.js +18 -0
- package/tmp/test-org-search.js +55 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Tasks: Add Automated Testing
|
|
2
|
+
|
|
3
|
+
Ordered list of work items to add automated tests to the project.
|
|
4
|
+
|
|
5
|
+
## Phase 1: Foundation
|
|
6
|
+
|
|
7
|
+
- [x] Install Vitest and related dependencies
|
|
8
|
+
- [x] Create vitest.config.ts with TypeScript support
|
|
9
|
+
- [x] Create test directory structure: `tests/unit/`, `tests/integration/`, `tests/fixtures/`
|
|
10
|
+
- [x] Add test scripts to package.json (test, test:watch, test:coverage)
|
|
11
|
+
- [x] Create .gitignore entry for coverage directory
|
|
12
|
+
- [x] Configure coverage settings (threshold: 80%)
|
|
13
|
+
|
|
14
|
+
## Phase 2: Create Mock Fixtures
|
|
15
|
+
|
|
16
|
+
- [x] Create `tests/fixtures/responses/status-success.json`
|
|
17
|
+
- [x] Create `tests/fixtures/responses/package-search-success.json`
|
|
18
|
+
- [x] Create `tests/fixtures/responses/package-show-success.json`
|
|
19
|
+
- [x] Create `tests/fixtures/responses/organization-list-success.json`
|
|
20
|
+
- [x] Create `tests/fixtures/responses/organization-show-success.json`
|
|
21
|
+
- [x] Create `tests/fixtures/responses/organization-search-success.json`
|
|
22
|
+
- [x] Create `tests/fixtures/responses/datastore-search-success.json`
|
|
23
|
+
- [x] Create `tests/fixtures/errors/timeout.json`
|
|
24
|
+
- [x] Create `tests/fixtures/errors/not-found.json`
|
|
25
|
+
- [x] Create `tests/fixtures/errors/server-error.json`
|
|
26
|
+
- [x] Document fixture structure in tests/README.md
|
|
27
|
+
|
|
28
|
+
## Phase 3: Unit Tests for Utils
|
|
29
|
+
|
|
30
|
+
- [x] Create `tests/unit/formatting.test.ts`
|
|
31
|
+
- [x] Write test for truncateText() with normal text
|
|
32
|
+
- [x] Write test for truncateText() with empty string
|
|
33
|
+
- [x] Write test for truncateText() with text under limit
|
|
34
|
+
- [x] Write test for truncateText() with text over limit
|
|
35
|
+
- [x] Write test for formatDate() with ISO date string
|
|
36
|
+
- [x] Write test for formatDate() with null/undefined
|
|
37
|
+
- [x] Write test for formatBytes() with bytes
|
|
38
|
+
- [x] Write test for formatBytes() with kilobytes
|
|
39
|
+
- [x] Write test for formatBytes() with megabytes
|
|
40
|
+
- [x] Write test for formatBytes() with gigabytes
|
|
41
|
+
- [x] Run tests and verify all pass
|
|
42
|
+
|
|
43
|
+
- [x] Create `tests/unit/http.test.ts`
|
|
44
|
+
- [x] Write test for makeCkanRequest() successful response
|
|
45
|
+
- [x] Write test for makeCkanRequest() with error (404)
|
|
46
|
+
- [x] Write test for makeCkanRequest() with error (500)
|
|
47
|
+
- [x] Write test for makeCkanRequest() with timeout
|
|
48
|
+
- [x] Write test for makeCkanRequest() URL normalization (trailing slash)
|
|
49
|
+
- [x] Write test for makeCkanRequest() User-Agent header
|
|
50
|
+
- [x] Run tests and verify all pass
|
|
51
|
+
|
|
52
|
+
## Phase 4: Integration Tests for Tools
|
|
53
|
+
|
|
54
|
+
### Status Tool
|
|
55
|
+
- [x] Create `tests/integration/status.test.ts`
|
|
56
|
+
- [x] Write test for ckan_status_show with valid server URL
|
|
57
|
+
- [x] Write test for ckan_status_show with invalid URL
|
|
58
|
+
- [ ] Write test for ckan_status_show with server error
|
|
59
|
+
- [ ] Write test for markdown output format
|
|
60
|
+
- [ ] Write test for JSON output format
|
|
61
|
+
- [x] Run tests and verify all pass
|
|
62
|
+
|
|
63
|
+
### Package Tools
|
|
64
|
+
- [ ] Create `tests/integration/package.test.ts`
|
|
65
|
+
- [ ] Write test for ckan_package_search with basic query
|
|
66
|
+
- [ ] Write test for ckan_package_search with facets
|
|
67
|
+
- [ ] Write test for ckan_package_search with pagination
|
|
68
|
+
- [ ] Write test for ckan_package_search with sorting
|
|
69
|
+
- [ ] Write test for ckan_package_search with filter query
|
|
70
|
+
- [ ] Write test for ckan_package_show with valid package ID
|
|
71
|
+
- [ ] Write test for ckan_package_show with invalid package ID
|
|
72
|
+
- [ ] Write test for markdown output format
|
|
73
|
+
- [ ] Write test for JSON output format
|
|
74
|
+
- [ ] Run tests and verify all pass
|
|
75
|
+
|
|
76
|
+
### Organization Tools
|
|
77
|
+
- [ ] Create `tests/integration/organization.test.ts`
|
|
78
|
+
- [ ] Write test for ckan_organization_list
|
|
79
|
+
- [ ] Write test for ckan_organization_list with sorting
|
|
80
|
+
- [ ] Write test for ckan_organization_show
|
|
81
|
+
- [ ] Write test for ckan_organization_search
|
|
82
|
+
- [ ] Write test for markdown output format
|
|
83
|
+
- [ ] Write test for JSON output format
|
|
84
|
+
- [ ] Run tests and verify all pass
|
|
85
|
+
|
|
86
|
+
### DataStore Tool
|
|
87
|
+
- [ ] Create `tests/integration/datastore.test.ts`
|
|
88
|
+
- [ ] Write test for ckan_datastore_search with basic query
|
|
89
|
+
- [ ] Write test for ckan_datastore_search with filters
|
|
90
|
+
- [ ] Write test for ckan_datastore_search with sorting
|
|
91
|
+
- [ ] Write test for ckan_datastore_search with pagination
|
|
92
|
+
- [ ] Write test for markdown output format
|
|
93
|
+
- [ ] Write test for JSON output format
|
|
94
|
+
- [ ] Run tests and verify all pass
|
|
95
|
+
|
|
96
|
+
## Phase 5: Validation and Documentation
|
|
97
|
+
|
|
98
|
+
- [x] Run `npm test` and verify all tests pass
|
|
99
|
+
- [x] Run `npm run test:coverage` and check coverage report
|
|
100
|
+
- [ ] Verify coverage meets 80% threshold
|
|
101
|
+
- [ ] Update CLAUDE.md with testing instructions
|
|
102
|
+
- [ ] Update README.md with test section
|
|
103
|
+
- [x] Create test writing guide in tests/README.md
|
|
104
|
+
- [ ] Add example test in test writing guide
|
|
105
|
+
|
|
106
|
+
## Phase 6: Continuous Integration
|
|
107
|
+
|
|
108
|
+
- [ ] Check if CI/CD exists (GitHub Actions, etc.)
|
|
109
|
+
- [ ] Add test step to CI pipeline if exists
|
|
110
|
+
- [ ] Add coverage reporting to CI if exists
|
|
111
|
+
- [ ] Verify tests run in CI environment
|
|
112
|
+
- [ ] Configure coverage upload to service (optional)
|
|
113
|
+
|
|
114
|
+
## Dependencies
|
|
115
|
+
|
|
116
|
+
- Phase 1 must complete before Phase 2, 3, 4
|
|
117
|
+
- Phase 2 must complete before Phase 4 (fixtures needed)
|
|
118
|
+
- Phase 3 and Phase 4 can be done in parallel after Phase 2
|
|
119
|
+
- Phase 5 depends on completion of Phase 3 and Phase 4
|
|
120
|
+
- Phase 6 is optional and depends on CI infrastructure
|
|
121
|
+
|
|
122
|
+
## Notes
|
|
123
|
+
|
|
124
|
+
- Use vi.fn() for mocking axios
|
|
125
|
+
- Keep tests focused and readable
|
|
126
|
+
- Mock realistic CKAN API responses from actual documentation
|
|
127
|
+
- One assertion per test when possible
|
|
128
|
+
- Describe expected behavior clearly in test names
|
|
129
|
+
- Use describe blocks for grouping related tests
|
|
130
|
+
- Use test.only sparingly and remove before committing
|
|
131
|
+
- Run tests in watch mode during development: `npm run test:watch`
|
|
132
|
+
- Generate coverage report: `npm run test:coverage`
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Project Context
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
CKAN MCP Server - MCP (Model Context Protocol) server for interacting with open data portals based on CKAN (dati.gov.it, data.gov, demo.ckan.org, etc.).
|
|
5
|
+
|
|
6
|
+
Exposes MCP tools for:
|
|
7
|
+
- Advanced dataset search with Solr syntax
|
|
8
|
+
- DataStore queries for tabular data analysis
|
|
9
|
+
- Organization and group exploration
|
|
10
|
+
- Complete metadata access
|
|
11
|
+
|
|
12
|
+
## Tech Stack
|
|
13
|
+
- **TypeScript** (ES2022 target)
|
|
14
|
+
- **Node.js**
|
|
15
|
+
- **esbuild** - ultra-fast build system (preferred over tsc for memory efficiency)
|
|
16
|
+
- **@modelcontextprotocol/sdk** - official MCP SDK
|
|
17
|
+
- **axios** - HTTP client for CKAN API calls
|
|
18
|
+
- **zod** - schema validation for tool inputs
|
|
19
|
+
- **express** - HTTP server (for HTTP transport mode)
|
|
20
|
+
|
|
21
|
+
## Project Conventions
|
|
22
|
+
|
|
23
|
+
### Code Style
|
|
24
|
+
- TypeScript strict mode enabled
|
|
25
|
+
- Single-file architecture (src/index.ts)
|
|
26
|
+
- Utility functions for reusable operations (makeCkanRequest, truncateText, formatDate, formatBytes)
|
|
27
|
+
- Zod schemas for strict input validation (reject extra parameters)
|
|
28
|
+
- Italian locale for date formatting (it-IT)
|
|
29
|
+
- Markdown output optimized for human readability; JSON available for programmatic use
|
|
30
|
+
|
|
31
|
+
### Architecture Patterns
|
|
32
|
+
- **Single-file MCP server** - all logic in src/index.ts
|
|
33
|
+
- **Tool-based architecture** - 7 registered MCP tools for CKAN operations
|
|
34
|
+
- **Dual transport modes**:
|
|
35
|
+
- stdio (default) - for local MCP client integration
|
|
36
|
+
- HTTP - for remote access via POST /mcp endpoint
|
|
37
|
+
- **Read-only operations** - no data modification on CKAN
|
|
38
|
+
- **No caching** - fresh API calls for each request
|
|
39
|
+
- **Error handling** - HTTP errors, timeouts, server validation
|
|
40
|
+
|
|
41
|
+
### Testing Strategy
|
|
42
|
+
- **No automated tests currently** (consider adding for critical tools)
|
|
43
|
+
- Manual testing:
|
|
44
|
+
- stdio mode: direct integration testing with Claude Desktop
|
|
45
|
+
- HTTP mode: curl-based testing against localhost endpoint
|
|
46
|
+
- Build validation via `npm run build` before deployment
|
|
47
|
+
|
|
48
|
+
### Git Workflow
|
|
49
|
+
- **Main branch**: `main`
|
|
50
|
+
- **Commit style**: extremely concise, prioritize brevity over grammar
|
|
51
|
+
- **LOG.md**: date-based entries (YYYY-MM-DD format), most recent at top
|
|
52
|
+
- **GitHub operations**: use `gh` CLI exclusively
|
|
53
|
+
- Update LOG.md for any significant change
|
|
54
|
+
|
|
55
|
+
## Domain Context
|
|
56
|
+
|
|
57
|
+
### CKAN Knowledge
|
|
58
|
+
- **CKAN API v3** - standard API across all CKAN portals
|
|
59
|
+
- **Apache Solr** - powers search functionality
|
|
60
|
+
- Supports field:value syntax, AND/OR/NOT operators, wildcards, ranges
|
|
61
|
+
- Filter queries (fq) for filtering without affecting score
|
|
62
|
+
- Facets for statistical aggregations
|
|
63
|
+
- Sort and pagination (start/rows)
|
|
64
|
+
|
|
65
|
+
### Output Conventions
|
|
66
|
+
- All tools support `format` parameter: `markdown` (default) or `json`
|
|
67
|
+
- Output truncated to 50000 characters max (hardcoded)
|
|
68
|
+
- Dates formatted in Italian locale
|
|
69
|
+
- File sizes in human-readable format
|
|
70
|
+
|
|
71
|
+
## Important Constraints
|
|
72
|
+
|
|
73
|
+
### Environment
|
|
74
|
+
- **WSL considerations** - memory constraints require esbuild instead of tsc
|
|
75
|
+
- Always use `npm run build` (esbuild) not `npm run build:tsc`
|
|
76
|
+
- esbuild bundles internal modules but keeps external deps as-is
|
|
77
|
+
|
|
78
|
+
### Technical Limits
|
|
79
|
+
- 50000 character output limit (hardcoded, could be configurable)
|
|
80
|
+
- 30-second timeout for CKAN API calls
|
|
81
|
+
- Read-only operations only (security by design)
|
|
82
|
+
- No authentication (public endpoints only)
|
|
83
|
+
- No caching (trade-off: freshness vs performance)
|
|
84
|
+
|
|
85
|
+
### Build System
|
|
86
|
+
- esbuild config: bundles TypeScript, externalizes runtime deps
|
|
87
|
+
- External deps must be in node_modules: @modelcontextprotocol/sdk, axios, express, zod
|
|
88
|
+
- tsconfig.json mainly for IDE support (esbuild ignores most options)
|
|
89
|
+
|
|
90
|
+
## External Dependencies
|
|
91
|
+
|
|
92
|
+
### CKAN Portals
|
|
93
|
+
Major supported portals:
|
|
94
|
+
- 🇮🇹 https://dati.gov.it (Italia)
|
|
95
|
+
- 🇺🇸 https://catalog.data.gov (United States)
|
|
96
|
+
- 🇨🇦 https://open.canada.ca/data (Canada)
|
|
97
|
+
- 🇬🇧 https://data.gov.uk (United Kingdom)
|
|
98
|
+
- 🇪🇺 https://data.europa.eu (European Union)
|
|
99
|
+
- 🌍 https://demo.ckan.org (Official CKAN demo)
|
|
100
|
+
|
|
101
|
+
### CKAN API Integration
|
|
102
|
+
- All requests use axios with 30s timeout
|
|
103
|
+
- User-Agent: `CKAN-MCP-Server/1.0`
|
|
104
|
+
- URL normalization (remove trailing slash)
|
|
105
|
+
- Response validation (success === true)
|
|
106
|
+
- Error handling: HTTP errors, timeouts, unreachable servers
|
|
107
|
+
|
|
108
|
+
### Portal Variations
|
|
109
|
+
Each CKAN portal may differ in:
|
|
110
|
+
- DataStore availability
|
|
111
|
+
- Custom dataset fields
|
|
112
|
+
- Available organizations and tags
|
|
113
|
+
- Supported resource formats
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# documentation-language Specification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
TBD - created by archiving change translate-project-to-english. Update Purpose after archive.
|
|
5
|
+
## Requirements
|
|
6
|
+
### Requirement: Project documentation in English
|
|
7
|
+
|
|
8
|
+
The project SHALL provide all user-facing documentation in English to ensure accessibility for the international open data community.
|
|
9
|
+
|
|
10
|
+
#### Scenario: English documentation
|
|
11
|
+
Given a CKAN MCP Server project
|
|
12
|
+
When a user reads the documentation
|
|
13
|
+
Then the documentation is in English
|
|
14
|
+
And code examples are in English
|
|
15
|
+
And technical terms follow CKAN API terminology
|
|
16
|
+
|
|
17
|
+
### Requirement: Preserve multilingual examples
|
|
18
|
+
|
|
19
|
+
The project SHALL preserve non-English portal names, organization titles, or data values in their original language to accurately reflect real-world CKAN portals.
|
|
20
|
+
|
|
21
|
+
#### Scenario: Italian portal example
|
|
22
|
+
Given an example using dati.gov.it
|
|
23
|
+
When the example references Italian organizations
|
|
24
|
+
Then organization names remain in Italian (e.g., "Regione Siciliana")
|
|
25
|
+
And descriptions are in English
|
|
26
|
+
|
|
27
|
+
#### Scenario: Non-English data values
|
|
28
|
+
Given an example query with data filters
|
|
29
|
+
When the data contains non-English values
|
|
30
|
+
Then those values are preserved in their original language
|
|
31
|
+
And the surrounding documentation explains the context in English
|
|
32
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# mcp-resources Specification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
TBD - created by archiving change add-mcp-resources. Update Purpose after archive.
|
|
5
|
+
## Requirements
|
|
6
|
+
### Requirement: Dataset Resource Template
|
|
7
|
+
|
|
8
|
+
The system SHALL expose a resource template for accessing CKAN dataset metadata via the URI pattern `ckan://{server}/dataset/{id}`.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Read dataset metadata successfully
|
|
11
|
+
|
|
12
|
+
- **WHEN** client reads `ckan://dati.gov.it/dataset/vaccini-covid`
|
|
13
|
+
- **THEN** server returns JSON with complete dataset metadata including title, description, resources, organization, tags
|
|
14
|
+
|
|
15
|
+
#### Scenario: Dataset not found
|
|
16
|
+
|
|
17
|
+
- **WHEN** client reads `ckan://demo.ckan.org/dataset/nonexistent-id`
|
|
18
|
+
- **THEN** server returns error indicating dataset not found
|
|
19
|
+
|
|
20
|
+
#### Scenario: Server unreachable
|
|
21
|
+
|
|
22
|
+
- **WHEN** client reads `ckan://invalid-server.example/dataset/test`
|
|
23
|
+
- **THEN** server returns error indicating server unreachable
|
|
24
|
+
|
|
25
|
+
### Requirement: Resource Resource Template
|
|
26
|
+
|
|
27
|
+
The system SHALL expose a resource template for accessing CKAN resource metadata via the URI pattern `ckan://{server}/resource/{id}`.
|
|
28
|
+
|
|
29
|
+
#### Scenario: Read resource metadata successfully
|
|
30
|
+
|
|
31
|
+
- **WHEN** client reads `ckan://dati.gov.it/resource/abc-123-def`
|
|
32
|
+
- **THEN** server returns JSON with resource metadata including name, format, URL, size, and download link
|
|
33
|
+
|
|
34
|
+
#### Scenario: Resource not found
|
|
35
|
+
|
|
36
|
+
- **WHEN** client reads `ckan://demo.ckan.org/resource/invalid-id`
|
|
37
|
+
- **THEN** server returns error indicating resource not found
|
|
38
|
+
|
|
39
|
+
### Requirement: Organization Resource Template
|
|
40
|
+
|
|
41
|
+
The system SHALL expose a resource template for accessing CKAN organization metadata via the URI pattern `ckan://{server}/organization/{name}`.
|
|
42
|
+
|
|
43
|
+
#### Scenario: Read organization metadata successfully
|
|
44
|
+
|
|
45
|
+
- **WHEN** client reads `ckan://dati.gov.it/organization/regione-toscana`
|
|
46
|
+
- **THEN** server returns JSON with organization metadata including title, description, and dataset count
|
|
47
|
+
|
|
48
|
+
#### Scenario: Organization not found
|
|
49
|
+
|
|
50
|
+
- **WHEN** client reads `ckan://demo.ckan.org/organization/nonexistent-org`
|
|
51
|
+
- **THEN** server returns error indicating organization not found
|
|
52
|
+
|
|
53
|
+
### Requirement: URI Scheme Parsing
|
|
54
|
+
|
|
55
|
+
The system SHALL parse `ckan://` URIs extracting server hostname and path components.
|
|
56
|
+
|
|
57
|
+
#### Scenario: Parse standard URI
|
|
58
|
+
|
|
59
|
+
- **WHEN** URI is `ckan://dati.gov.it/dataset/test-id`
|
|
60
|
+
- **THEN** server extracts: server=`https://dati.gov.it`, type=`dataset`, id=`test-id`
|
|
61
|
+
|
|
62
|
+
#### Scenario: Parse URI with www prefix
|
|
63
|
+
|
|
64
|
+
- **WHEN** URI is `ckan://www.dati.gov.it/dataset/test-id`
|
|
65
|
+
- **THEN** server extracts: server=`https://www.dati.gov.it`, type=`dataset`, id=`test-id`
|
|
66
|
+
|
|
67
|
+
#### Scenario: Reject malformed URI
|
|
68
|
+
|
|
69
|
+
- **WHEN** URI is `ckan://invalid` (missing path)
|
|
70
|
+
- **THEN** server returns validation error
|
|
71
|
+
|
|
72
|
+
### Requirement: Resource Response Format
|
|
73
|
+
|
|
74
|
+
The system SHALL return resource content as JSON with standard MCP resource response structure.
|
|
75
|
+
|
|
76
|
+
#### Scenario: JSON response structure
|
|
77
|
+
|
|
78
|
+
- **WHEN** client reads any valid resource URI
|
|
79
|
+
- **THEN** response contains `contents` array with `uri`, `mimeType` (application/json), and `text` (JSON string)
|
|
80
|
+
|
|
81
|
+
#### Scenario: Large response truncation
|
|
82
|
+
|
|
83
|
+
- **WHEN** resource content exceeds CHARACTER_LIMIT
|
|
84
|
+
- **THEN** response is truncated to CHARACTER_LIMIT with truncation indicator
|
|
85
|
+
|
|
86
|
+
### Requirement: Resource Discovery
|
|
87
|
+
|
|
88
|
+
The system SHALL list available resource templates when client requests resource list.
|
|
89
|
+
|
|
90
|
+
#### Scenario: List resource templates
|
|
91
|
+
|
|
92
|
+
- **WHEN** client calls `resources/listTemplates`
|
|
93
|
+
- **THEN** server returns list of available URI templates with descriptions
|
|
94
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aborruso/ckan-mcp-server",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "MCP server for interacting with CKAN open data portals",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/sdk --external:axios --external:express --external:zod",
|
|
9
|
+
"build:tsc": "node --max-old-space-size=8192 ./node_modules/typescript/bin/tsc",
|
|
10
|
+
"start": "node dist/index.js",
|
|
11
|
+
"dev": "npm run build && node dist/index.js",
|
|
12
|
+
"watch": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/sdk --external:axios --external:express --external:zod --watch",
|
|
13
|
+
"test": "vitest",
|
|
14
|
+
"test:watch": "vitest --watch",
|
|
15
|
+
"test:coverage": "vitest --coverage"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"ckan",
|
|
20
|
+
"open-data",
|
|
21
|
+
"api",
|
|
22
|
+
"data-portal"
|
|
23
|
+
],
|
|
24
|
+
"author": "Andrea Borruso <aborruso@gmail.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
28
|
+
"axios": "^1.7.2",
|
|
29
|
+
"express": "^4.19.2",
|
|
30
|
+
"zod": "^3.23.8"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/express": "^4.17.21",
|
|
34
|
+
"@types/node": "^20.12.12",
|
|
35
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
36
|
+
"esbuild": "^0.27.2",
|
|
37
|
+
"typescript": "^5.4.5",
|
|
38
|
+
"vitest": "^4.0.16"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/spunti.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Intro
|
|
2
|
+
|
|
3
|
+
Spunti vari
|
|
4
|
+
|
|
5
|
+
## Da fare
|
|
6
|
+
|
|
7
|
+
- farlo totalmente in inglese
|
|
8
|
+
- documentarne l'uso per chatGPT e Claude Code
|
|
9
|
+
- aggiunere agli esempi e al tool poter cercare per frequenza di aggiornamento
|
|
10
|
+
- il tema del peso del ranking dei risultati andrebbe approfondito. Prendere spunto da ckan canada
|
|
11
|
+
|
|
12
|
+
## Tool con coi confrontarsi
|
|
13
|
+
|
|
14
|
+
- https://skywork.ai/skypage/en/unlocking-government-data-mcp-server/1980445961155629056
|
|
15
|
+
- https://github.com/ondics/ckan-mcp-server
|
|
16
|
+
|
|
17
|
+
## Temi aperti
|
|
18
|
+
|
|
19
|
+
- la data di modifica di una risorsa sembra non esserci realmente, salvo non cambiare i metadti. Se si aggiorna soltanto il csv non ce ne è traccia?
|
package/tasks/todo.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Task: Aggiungere tool ckan_organization_search
|
|
2
|
+
|
|
3
|
+
## Obiettivo
|
|
4
|
+
|
|
5
|
+
Implementare un nuovo tool MCP dedicato alla ricerca di organizzazioni per nome, con interfaccia più semplice rispetto a `package_search` con sintassi Solr.
|
|
6
|
+
|
|
7
|
+
## Fasi
|
|
8
|
+
|
|
9
|
+
### Fase 1: Implementazione tool
|
|
10
|
+
- [ ] Aggiungere `ckan_organization_search` in `src/index.ts` (prima riga 872)
|
|
11
|
+
- [ ] Schema input: `server_url`, `pattern`, `response_format`
|
|
12
|
+
- [ ] Costruire query Solr automatica: `organization:*{pattern}*`
|
|
13
|
+
- [ ] Chiamare `package_search` con `rows=0` e `facet.field=organization`
|
|
14
|
+
- [ ] Output JSON: `{ count: N, organizations: [...] }`
|
|
15
|
+
- [ ] Output markdown: tabella organizzazioni con conteggio dataset
|
|
16
|
+
|
|
17
|
+
### Fase 2: Testing
|
|
18
|
+
- [ ] Build: `npm run build`
|
|
19
|
+
- [ ] Test con pattern "toscana"
|
|
20
|
+
- [ ] Verificare output JSON
|
|
21
|
+
- [ ] Verificare output markdown
|
|
22
|
+
|
|
23
|
+
### Fase 3: Documentazione
|
|
24
|
+
- [ ] Aggiungere esempi in `EXAMPLES.md`
|
|
25
|
+
- [ ] Aggiornare `LOG.md` con data e modifiche
|
|
26
|
+
|
|
27
|
+
## Dettagli Implementazione
|
|
28
|
+
|
|
29
|
+
**Posizione**: Dopo tool `ckan_organization_show` (circa riga 700) e prima di `ckan_datastore_search`
|
|
30
|
+
|
|
31
|
+
**Firma tool**:
|
|
32
|
+
```typescript
|
|
33
|
+
server.registerTool(
|
|
34
|
+
"ckan_organization_search",
|
|
35
|
+
{
|
|
36
|
+
title: "Search CKAN Organizations by Name",
|
|
37
|
+
description: "...",
|
|
38
|
+
inputSchema: z.object({
|
|
39
|
+
server_url: z.string().url(),
|
|
40
|
+
pattern: z.string().min(1).describe("Search pattern (wildcards added automatically)"),
|
|
41
|
+
response_format: ResponseFormatSchema
|
|
42
|
+
}).strict(),
|
|
43
|
+
annotations: {
|
|
44
|
+
readOnlyHint: true,
|
|
45
|
+
destructiveHint: false,
|
|
46
|
+
idempotentHint: true,
|
|
47
|
+
openWorldHint: true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async (params) => { ... }
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Logica**:
|
|
55
|
+
1. Query Solr: `organization:*${params.pattern}*`
|
|
56
|
+
2. API call: `package_search` con `rows=0`, `facet.field=["organization"]`, `facet.limit=500`
|
|
57
|
+
3. Parsing facets per estrarre organizzazioni matchate
|
|
58
|
+
4. Output formattato
|
|
59
|
+
|
|
60
|
+
## Note
|
|
61
|
+
|
|
62
|
+
- Molto più semplice di costruire query manualmente
|
|
63
|
+
- Risparmio token: restituisce solo organizzazioni, non dataset
|
|
64
|
+
- Pattern matching automatico (l'utente non deve ricordare wildcard Solr)
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Review
|
|
69
|
+
|
|
70
|
+
### Modifiche Completate
|
|
71
|
+
|
|
72
|
+
**File modificati**:
|
|
73
|
+
1. `src/index.ts` - Aggiunto tool `ckan_organization_search` (righe 680-785)
|
|
74
|
+
2. `EXAMPLES.md` - Aggiunti esempi di utilizzo (righe 54-73)
|
|
75
|
+
3. `LOG.md` - Documentata la nuova feature
|
|
76
|
+
|
|
77
|
+
**Implementazione**:
|
|
78
|
+
- Tool registrato tra `ckan_organization_show` e `ckan_datastore_search`
|
|
79
|
+
- Schema input: `server_url`, `pattern`, `response_format`
|
|
80
|
+
- Query Solr automatica: `organization:*{pattern}*`
|
|
81
|
+
- Utilizza `package_search` con `rows=0` per efficienza massima
|
|
82
|
+
- Output JSON: `{ count, total_datasets, organizations: [...] }`
|
|
83
|
+
- Output markdown: tabella formattata con org e conteggio dataset
|
|
84
|
+
|
|
85
|
+
**Testing**:
|
|
86
|
+
- Build completato con successo (47ms, esbuild)
|
|
87
|
+
- Test manuale con pattern "toscana": ✅
|
|
88
|
+
- 2 organizzazioni trovate
|
|
89
|
+
- 11.000 dataset totali
|
|
90
|
+
- Output JSON corretto
|
|
91
|
+
- Zero dataset scaricati (solo facet)
|
|
92
|
+
|
|
93
|
+
**Vantaggi**:
|
|
94
|
+
- **UX migliorata**: API semplice vs sintassi Solr
|
|
95
|
+
- **Efficienza**: filtraggio lato server, zero dataset trasmessi
|
|
96
|
+
- **Token saving**: solo metadata organizzazioni
|
|
97
|
+
- **Performance**: wildcard automatici, no costruzione query manuale
|
|
98
|
+
|
|
99
|
+
### Prossimi Passi
|
|
100
|
+
|
|
101
|
+
Per utilizzare il tool:
|
|
102
|
+
1. Riavviare Claude Code per caricare il nuovo server MCP
|
|
103
|
+
2. Il tool sarà disponibile come `ckan_organization_search`
|
|
104
|
+
|
|
105
|
+
### Esempio Output
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"count": 2,
|
|
110
|
+
"total_datasets": 11000,
|
|
111
|
+
"organizations": [
|
|
112
|
+
{
|
|
113
|
+
"name": "regione-toscana",
|
|
114
|
+
"display_name": "Regione Toscana",
|
|
115
|
+
"dataset_count": 10988
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "autorita-idrica-toscana",
|
|
119
|
+
"display_name": "Autorità Idrica Toscana",
|
|
120
|
+
"dataset_count": 12
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
```
|
package/test-urls.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getDatasetViewUrl, getOrganizationViewUrl } from './src/utils/url-generator.js';
|
|
2
|
+
|
|
3
|
+
const serverUrl = 'https://www.dati.gov.it/opendata';
|
|
4
|
+
const pkg = {
|
|
5
|
+
id: 'f20b37d0cd56764f77e4c707cc62b528eeb48ff24cdb415174a0dd31b63cad0c',
|
|
6
|
+
name: 'test-dataset'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const org = {
|
|
10
|
+
id: 'org-id',
|
|
11
|
+
name: 'comune-di-bari'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
console.log('Dataset URL:', getDatasetViewUrl(serverUrl, pkg));
|
|
15
|
+
console.log('Organization URL:', getOrganizationViewUrl(serverUrl, org));
|
|
16
|
+
|
|
17
|
+
const otherServer = 'https://demo.ckan.org';
|
|
18
|
+
console.log('Other Dataset URL:', getDatasetViewUrl(otherServer, pkg));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Test manuale del tool ckan_organization_search
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
async function testOrgSearch() {
|
|
5
|
+
const serverUrl = 'https://www.dati.gov.it/opendata';
|
|
6
|
+
const pattern = 'toscana';
|
|
7
|
+
const query = `organization:*${pattern}*`;
|
|
8
|
+
|
|
9
|
+
console.log('Testing organization search...');
|
|
10
|
+
console.log('Pattern:', pattern);
|
|
11
|
+
console.log('Query:', query);
|
|
12
|
+
console.log('');
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const url = `${serverUrl}/api/3/action/package_search`;
|
|
16
|
+
const response = await axios.get(url, {
|
|
17
|
+
params: {
|
|
18
|
+
q: query,
|
|
19
|
+
rows: 0,
|
|
20
|
+
'facet.field': JSON.stringify(['organization']),
|
|
21
|
+
'facet.limit': 500
|
|
22
|
+
},
|
|
23
|
+
timeout: 30000,
|
|
24
|
+
headers: {
|
|
25
|
+
'User-Agent': 'CKAN-MCP-Server/1.0'
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const result = response.data.result;
|
|
30
|
+
const orgFacets = result.search_facets?.organization?.items || [];
|
|
31
|
+
const totalDatasets = result.count || 0;
|
|
32
|
+
|
|
33
|
+
console.log('Organizations found:', orgFacets.length);
|
|
34
|
+
console.log('Total datasets:', totalDatasets);
|
|
35
|
+
console.log('');
|
|
36
|
+
|
|
37
|
+
const jsonResult = {
|
|
38
|
+
count: orgFacets.length,
|
|
39
|
+
total_datasets: totalDatasets,
|
|
40
|
+
organizations: orgFacets.map((item) => ({
|
|
41
|
+
name: item.name,
|
|
42
|
+
display_name: item.display_name,
|
|
43
|
+
dataset_count: item.count
|
|
44
|
+
}))
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
console.log(JSON.stringify(jsonResult, null, 2));
|
|
48
|
+
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error:', error.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
testOrgSearch();
|