abs-mcp 0.1.0__tar.gz

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.
Files changed (38) hide show
  1. abs_mcp-0.1.0/.github/workflows/test.yml +47 -0
  2. abs_mcp-0.1.0/.gitignore +24 -0
  3. abs_mcp-0.1.0/CHANGELOG.md +12 -0
  4. abs_mcp-0.1.0/LICENSE +21 -0
  5. abs_mcp-0.1.0/PKG-INFO +188 -0
  6. abs_mcp-0.1.0/README.md +156 -0
  7. abs_mcp-0.1.0/examples/claude_desktop_config.json +8 -0
  8. abs_mcp-0.1.0/glama.json +4 -0
  9. abs_mcp-0.1.0/pyproject.toml +56 -0
  10. abs_mcp-0.1.0/src/abs_mcp/__init__.py +1 -0
  11. abs_mcp-0.1.0/src/abs_mcp/cache.py +87 -0
  12. abs_mcp-0.1.0/src/abs_mcp/catalog.py +129 -0
  13. abs_mcp-0.1.0/src/abs_mcp/client.py +109 -0
  14. abs_mcp-0.1.0/src/abs_mcp/curated.py +189 -0
  15. abs_mcp-0.1.0/src/abs_mcp/data/curated/ABS_ANNUAL_ERP_ASGS2021.yaml +49 -0
  16. abs_mcp-0.1.0/src/abs_mcp/data/curated/BA_GCCSA.yaml +71 -0
  17. abs_mcp-0.1.0/src/abs_mcp/data/curated/CPI.yaml +56 -0
  18. abs_mcp-0.1.0/src/abs_mcp/data/curated/JV.yaml +76 -0
  19. abs_mcp-0.1.0/src/abs_mcp/data/curated/LEND_HOUSING.yaml +72 -0
  20. abs_mcp-0.1.0/src/abs_mcp/data/curated/LF.yaml +54 -0
  21. abs_mcp-0.1.0/src/abs_mcp/data/curated/WPI.yaml +79 -0
  22. abs_mcp-0.1.0/src/abs_mcp/models.py +53 -0
  23. abs_mcp-0.1.0/src/abs_mcp/py.typed +0 -0
  24. abs_mcp-0.1.0/src/abs_mcp/server.py +237 -0
  25. abs_mcp-0.1.0/src/abs_mcp/shaping.py +252 -0
  26. abs_mcp-0.1.0/tests/__init__.py +0 -0
  27. abs_mcp-0.1.0/tests/conftest.py +16 -0
  28. abs_mcp-0.1.0/tests/fixtures/dataflows_min.xml +44927 -0
  29. abs_mcp-0.1.0/tests/fixtures/lf_dsd.xml +1731 -0
  30. abs_mcp-0.1.0/tests/fixtures/lf_one_obs.xml +1 -0
  31. abs_mcp-0.1.0/tests/test_cache.py +51 -0
  32. abs_mcp-0.1.0/tests/test_catalog.py +83 -0
  33. abs_mcp-0.1.0/tests/test_client.py +74 -0
  34. abs_mcp-0.1.0/tests/test_curated.py +123 -0
  35. abs_mcp-0.1.0/tests/test_integration.py +190 -0
  36. abs_mcp-0.1.0/tests/test_mcp_protocol.py +105 -0
  37. abs_mcp-0.1.0/tests/test_shaping.py +141 -0
  38. abs_mcp-0.1.0/uv.lock +1959 -0
@@ -0,0 +1,47 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.11", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v3
20
+ with:
21
+ enable-cache: true
22
+ - name: Set up Python ${{ matrix.python-version }}
23
+ run: uv python install ${{ matrix.python-version }}
24
+ - name: Sync dependencies
25
+ run: uv sync --extra dev
26
+ - name: Install package
27
+ run: uv pip install -e .
28
+ - name: Run unit tests
29
+ run: uv run pytest -q
30
+
31
+ build:
32
+ runs-on: ubuntu-latest
33
+ needs: test
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@v3
38
+ - name: Build wheel + sdist
39
+ run: uv build
40
+ - name: Verify wheel installs cleanly
41
+ run: |
42
+ uv run --isolated --with ./dist/*.whl python -c \
43
+ "import abs_mcp.server as s; assert len(s.list_curated()) == 5; print('OK')"
44
+ - uses: actions/upload-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: dist/
@@ -0,0 +1,24 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .mypy_cache/
12
+ .coverage
13
+ htmlcov/
14
+ *.swp
15
+ .DS_Store
16
+ .env
17
+
18
+ # Inspection-only DSDs (regenerate with: curl -H "Accept: application/vnd.sdmx.structure+xml;version=2.1" https://data.api.abs.gov.au/rest/datastructure/ABS/<ID>?references=all)
19
+ tests/fixtures/ABS_ANNUAL_ERP_ASGS2021_dsd.xml
20
+ tests/fixtures/BA_GCCSA_dsd.xml
21
+ tests/fixtures/CPI_dsd.xml
22
+ tests/fixtures/JV_dsd.xml
23
+ tests/fixtures/LEND_HOUSING_dsd.xml
24
+ tests/fixtures/WPI_dsd.xml
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-05-11)
4
+
5
+ Initial release.
6
+
7
+ - 5 MCP tools: `search_datasets`, `describe_dataset`, `get_data`, `latest`, `list_curated`
8
+ - Hand-curated plain-English mappings for 5 dataflows: `LF` (Labour Force), `CPI` (Consumer Price Index), `ABS_ANNUAL_ERP_ASGS2021` (Estimated Resident Population), `BA_GCCSA` (Building Approvals), `LEND_HOUSING` (Lending Indicators - Housing)
9
+ - Non-curated dataflows still queryable via raw SDMX dimension IDs and codes
10
+ - SQLite-backed cache with per-kind TTL: catalogue 24 h, codelists 7 d, data 1 h, latest 15 min
11
+ - Response shapes: `records` (default), `series`, `csv`
12
+ - 50 tests (35 unit + 7 live integration + 8 MCP-protocol)
abs_mcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harry Vass
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
abs_mcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: abs-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for the Australian Bureau of Statistics Data API. Hides SDMX behind plain-English tools, with curated mappings for Labour Force, CPI, ERP, Building Approvals, and Lending Indicators.
5
+ Project-URL: Homepage, https://github.com/Bigred97/abs-mcp
6
+ Project-URL: Issues, https://github.com/Bigred97/abs-mcp/issues
7
+ Author: Harry Vass
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: abs,australia,claude,mcp,sdmx,statistics
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: aiosqlite>=0.20
20
+ Requires-Dist: fastmcp<4,>=2.0
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pandas<3,>=2.2
23
+ Requires-Dist: pydantic>=2.7
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: rapidfuzz>=3.9
26
+ Requires-Dist: sdmx1>=2.20
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Requires-Dist: respx>=0.21; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # abs-mcp
34
+
35
+ An MCP server that wraps the [Australian Bureau of Statistics Data API](https://data.api.abs.gov.au/) and hides SDMX behind plain-English tools. Ask Claude "What's the unemployment rate in NSW?" and get a real answer with a source link, instead of a wall of SDMX codes.
36
+
37
+ Five tools, hand-curated mappings for Labour Force, CPI, Estimated Resident Population, Building Approvals, and Lending Indicators.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ # After publish:
43
+ uvx abs-mcp
44
+
45
+ # Local dev install:
46
+ uv pip install -e .
47
+ ```
48
+
49
+ ### Claude Desktop
50
+
51
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "abs": {
57
+ "command": "uvx",
58
+ "args": ["abs-mcp"]
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ For a local checkout (before PyPI publish):
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "abs": {
70
+ "command": "uv",
71
+ "args": ["run", "--directory", "/absolute/path/to/abs-mcp", "abs-mcp"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ Restart Claude Desktop. The `abs` server appears in the tools panel with five tools.
78
+
79
+ ### Cursor
80
+
81
+ Add to `~/.cursor/mcp.json` (or workspace `.cursor/mcp.json`):
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "abs": {
87
+ "command": "uvx",
88
+ "args": ["abs-mcp"]
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Tools
95
+
96
+ | Tool | What it does |
97
+ |---|---|
98
+ | `search_datasets(query, limit=10)` | Fuzzy-search ABS dataflow names. Returns the top matches. |
99
+ | `describe_dataset(dataset_id)` | Plain-English description of a dataflow's dimensions and values. |
100
+ | `get_data(dataset_id, filters, start_period, end_period, format)` | Query a dataflow with filters. Returns clean records (default), grouped series, or CSV. |
101
+ | `latest(dataset_id, filters)` | Just the most recent observation(s) — wraps `get_data` with `lastNObservations=1`. |
102
+ | `list_curated()` | The five dataflow IDs that have hand-curated plain-English support. |
103
+
104
+ ## Curated dataflows
105
+
106
+ For these five, `filters` accepts plain-English values (e.g. `"region": "nsw"` instead of `"REGION": "1"`):
107
+
108
+ - **LF** — Labour Force, monthly: employment, unemployment, participation by state/sex
109
+ - **CPI** — Consumer Price Index, quarterly inflation by capital city and category
110
+ - **ABS_ANNUAL_ERP_ASGS2021** — Estimated Resident Population, annual by state and sub-state geography
111
+ - **BA_GCCSA** — Building Approvals, monthly by state/capital region and building type
112
+ - **LEND_HOUSING** — Lending Indicators, monthly housing finance commitments by purpose, lender, and state
113
+
114
+ Any other ABS dataflow still works — pass raw SDMX dimension IDs and codes.
115
+
116
+ ## Worked examples
117
+
118
+ **"What's the current unemployment rate in NSW?"**
119
+
120
+ Claude calls:
121
+ ```
122
+ latest(dataset_id="LF", filters={"region": "nsw", "measure": "unemployment_rate"})
123
+ ```
124
+
125
+ Returns:
126
+ ```json
127
+ {
128
+ "dataset_id": "LF",
129
+ "dataset_name": "Labour Force",
130
+ "query": {"region": "nsw", "measure": "unemployment_rate"},
131
+ "period": {"start": "2026-03", "end": "2026-03"},
132
+ "records": [
133
+ {
134
+ "period": "2026-03",
135
+ "value": 4.2,
136
+ "dimensions": {"measure": "Unemployment rate", "region": "New South Wales", "sex": "Persons"}
137
+ }
138
+ ],
139
+ "source": "Australian Bureau of Statistics",
140
+ "abs_url": "https://www.abs.gov.au/statistics/labour/employment-and-unemployment/labour-force-australia"
141
+ }
142
+ ```
143
+
144
+ **"Show me NSW housing approvals over the last two years"**
145
+
146
+ ```
147
+ get_data(dataset_id="BA_GCCSA", filters={"region": "nsw", "measure": "dwelling_units"}, start_period="2024")
148
+ ```
149
+
150
+ **"Compare quarterly CPI in Sydney vs Melbourne"**
151
+
152
+ ```
153
+ get_data(dataset_id="CPI", filters={"region": ["sydney", "melbourne"], "measure": "change_year"}, start_period="2023")
154
+ ```
155
+
156
+ ## Period formats
157
+
158
+ ABS uses different period formats per dataflow:
159
+ - Monthly (LF, BA_GCCSA, LEND_HOUSING): `"2026-03"`
160
+ - Quarterly (CPI): `"2024-Q1"`
161
+ - Annual (ABS_ANNUAL_ERP_ASGS2021): `"2024"`
162
+
163
+ Pass `start_period` / `end_period` in the matching format.
164
+
165
+ ## Development
166
+
167
+ ```bash
168
+ git clone https://github.com/Bigred97/abs-mcp.git
169
+ cd abs-mcp
170
+ uv sync --extra dev
171
+ uv pip install -e .
172
+
173
+ # Unit tests (no network)
174
+ uv run pytest
175
+
176
+ # Live integration tests (hits real ABS API)
177
+ uv run pytest -m live
178
+ ```
179
+
180
+ The SQLite cache lives at `~/.abs-mcp/cache.db`. Catalogue refreshes every 24h, codelists every 7 days, data responses every hour, latest 15 minutes. Delete the file to force a refresh.
181
+
182
+ ## How it differs from existing ABS MCP servers
183
+
184
+ The one existing community option (`seansoreilly/abs`) exposes a single `query_dataset` tool that passes raw SDMX through. This package offers semantic tools and curated mappings for the highest-value dataflows so an LLM can answer real questions without you needing to know what `M13.3.1599.20.1.M` means.
185
+
186
+ ## License
187
+
188
+ MIT — Harry Vass, 2026.
@@ -0,0 +1,156 @@
1
+ # abs-mcp
2
+
3
+ An MCP server that wraps the [Australian Bureau of Statistics Data API](https://data.api.abs.gov.au/) and hides SDMX behind plain-English tools. Ask Claude "What's the unemployment rate in NSW?" and get a real answer with a source link, instead of a wall of SDMX codes.
4
+
5
+ Five tools, hand-curated mappings for Labour Force, CPI, Estimated Resident Population, Building Approvals, and Lending Indicators.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # After publish:
11
+ uvx abs-mcp
12
+
13
+ # Local dev install:
14
+ uv pip install -e .
15
+ ```
16
+
17
+ ### Claude Desktop
18
+
19
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "abs": {
25
+ "command": "uvx",
26
+ "args": ["abs-mcp"]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ For a local checkout (before PyPI publish):
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "abs": {
38
+ "command": "uv",
39
+ "args": ["run", "--directory", "/absolute/path/to/abs-mcp", "abs-mcp"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ Restart Claude Desktop. The `abs` server appears in the tools panel with five tools.
46
+
47
+ ### Cursor
48
+
49
+ Add to `~/.cursor/mcp.json` (or workspace `.cursor/mcp.json`):
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "abs": {
55
+ "command": "uvx",
56
+ "args": ["abs-mcp"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## Tools
63
+
64
+ | Tool | What it does |
65
+ |---|---|
66
+ | `search_datasets(query, limit=10)` | Fuzzy-search ABS dataflow names. Returns the top matches. |
67
+ | `describe_dataset(dataset_id)` | Plain-English description of a dataflow's dimensions and values. |
68
+ | `get_data(dataset_id, filters, start_period, end_period, format)` | Query a dataflow with filters. Returns clean records (default), grouped series, or CSV. |
69
+ | `latest(dataset_id, filters)` | Just the most recent observation(s) — wraps `get_data` with `lastNObservations=1`. |
70
+ | `list_curated()` | The five dataflow IDs that have hand-curated plain-English support. |
71
+
72
+ ## Curated dataflows
73
+
74
+ For these five, `filters` accepts plain-English values (e.g. `"region": "nsw"` instead of `"REGION": "1"`):
75
+
76
+ - **LF** — Labour Force, monthly: employment, unemployment, participation by state/sex
77
+ - **CPI** — Consumer Price Index, quarterly inflation by capital city and category
78
+ - **ABS_ANNUAL_ERP_ASGS2021** — Estimated Resident Population, annual by state and sub-state geography
79
+ - **BA_GCCSA** — Building Approvals, monthly by state/capital region and building type
80
+ - **LEND_HOUSING** — Lending Indicators, monthly housing finance commitments by purpose, lender, and state
81
+
82
+ Any other ABS dataflow still works — pass raw SDMX dimension IDs and codes.
83
+
84
+ ## Worked examples
85
+
86
+ **"What's the current unemployment rate in NSW?"**
87
+
88
+ Claude calls:
89
+ ```
90
+ latest(dataset_id="LF", filters={"region": "nsw", "measure": "unemployment_rate"})
91
+ ```
92
+
93
+ Returns:
94
+ ```json
95
+ {
96
+ "dataset_id": "LF",
97
+ "dataset_name": "Labour Force",
98
+ "query": {"region": "nsw", "measure": "unemployment_rate"},
99
+ "period": {"start": "2026-03", "end": "2026-03"},
100
+ "records": [
101
+ {
102
+ "period": "2026-03",
103
+ "value": 4.2,
104
+ "dimensions": {"measure": "Unemployment rate", "region": "New South Wales", "sex": "Persons"}
105
+ }
106
+ ],
107
+ "source": "Australian Bureau of Statistics",
108
+ "abs_url": "https://www.abs.gov.au/statistics/labour/employment-and-unemployment/labour-force-australia"
109
+ }
110
+ ```
111
+
112
+ **"Show me NSW housing approvals over the last two years"**
113
+
114
+ ```
115
+ get_data(dataset_id="BA_GCCSA", filters={"region": "nsw", "measure": "dwelling_units"}, start_period="2024")
116
+ ```
117
+
118
+ **"Compare quarterly CPI in Sydney vs Melbourne"**
119
+
120
+ ```
121
+ get_data(dataset_id="CPI", filters={"region": ["sydney", "melbourne"], "measure": "change_year"}, start_period="2023")
122
+ ```
123
+
124
+ ## Period formats
125
+
126
+ ABS uses different period formats per dataflow:
127
+ - Monthly (LF, BA_GCCSA, LEND_HOUSING): `"2026-03"`
128
+ - Quarterly (CPI): `"2024-Q1"`
129
+ - Annual (ABS_ANNUAL_ERP_ASGS2021): `"2024"`
130
+
131
+ Pass `start_period` / `end_period` in the matching format.
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ git clone https://github.com/Bigred97/abs-mcp.git
137
+ cd abs-mcp
138
+ uv sync --extra dev
139
+ uv pip install -e .
140
+
141
+ # Unit tests (no network)
142
+ uv run pytest
143
+
144
+ # Live integration tests (hits real ABS API)
145
+ uv run pytest -m live
146
+ ```
147
+
148
+ The SQLite cache lives at `~/.abs-mcp/cache.db`. Catalogue refreshes every 24h, codelists every 7 days, data responses every hour, latest 15 minutes. Delete the file to force a refresh.
149
+
150
+ ## How it differs from existing ABS MCP servers
151
+
152
+ The one existing community option (`seansoreilly/abs`) exposes a single `query_dataset` tool that passes raw SDMX through. This package offers semantic tools and curated mappings for the highest-value dataflows so an LLM can answer real questions without you needing to know what `M13.3.1599.20.1.M` means.
153
+
154
+ ## License
155
+
156
+ MIT — Harry Vass, 2026.
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "abs": {
4
+ "command": "uvx",
5
+ "args": ["abs-mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": ["Bigred97"]
4
+ }
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "abs-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for the Australian Bureau of Statistics Data API. Hides SDMX behind plain-English tools, with curated mappings for Labour Force, CPI, ERP, Building Approvals, and Lending Indicators."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Harry Vass" }]
13
+ keywords = ["mcp", "abs", "statistics", "australia", "sdmx", "claude"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Scientific/Engineering :: Information Analysis",
22
+ ]
23
+ dependencies = [
24
+ "fastmcp>=2.0,<4",
25
+ "httpx>=0.27",
26
+ "pydantic>=2.7",
27
+ "rapidfuzz>=3.9",
28
+ "pandas>=2.2,<3",
29
+ "sdmx1>=2.20",
30
+ "aiosqlite>=0.20",
31
+ "PyYAML>=6.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=8",
37
+ "pytest-asyncio>=0.23",
38
+ "respx>=0.21",
39
+ ]
40
+
41
+ [project.scripts]
42
+ abs-mcp = "abs_mcp.server:main"
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/Bigred97/abs-mcp"
46
+ Issues = "https://github.com/Bigred97/abs-mcp/issues"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/abs_mcp"]
50
+
51
+ [tool.pytest.ini_options]
52
+ asyncio_mode = "auto"
53
+ markers = ["live: hits the real ABS API"]
54
+ addopts = "-m 'not live'"
55
+ testpaths = ["tests"]
56
+ pythonpath = ["src"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,87 @@
1
+ """SQLite-backed HTTP cache with per-read TTL.
2
+
3
+ Single table; the same row can satisfy different TTL windows because TTL is
4
+ evaluated at read time. The `kind` column lets us run targeted invalidation
5
+ later without renaming.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from datetime import timedelta
11
+ from pathlib import Path
12
+ from typing import Literal
13
+
14
+ import aiosqlite
15
+
16
+ CacheKind = Literal["catalogue", "datastructure", "data", "latest"]
17
+
18
+ DEFAULT_DB_PATH = Path.home() / ".abs-mcp" / "cache.db"
19
+
20
+ TTL: dict[CacheKind, timedelta] = {
21
+ "catalogue": timedelta(hours=24),
22
+ "datastructure": timedelta(days=7),
23
+ "data": timedelta(hours=1),
24
+ "latest": timedelta(minutes=15),
25
+ }
26
+
27
+ _SCHEMA = """
28
+ CREATE TABLE IF NOT EXISTS http_cache (
29
+ cache_key TEXT PRIMARY KEY,
30
+ payload BLOB NOT NULL,
31
+ cached_at REAL NOT NULL,
32
+ kind TEXT NOT NULL
33
+ );
34
+ CREATE INDEX IF NOT EXISTS idx_kind_cached_at ON http_cache(kind, cached_at);
35
+ """
36
+
37
+
38
+ class Cache:
39
+ def __init__(self, db_path: Path = DEFAULT_DB_PATH) -> None:
40
+ self.db_path = db_path
41
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
42
+ self._initialized = False
43
+
44
+ async def _ensure_init(self) -> None:
45
+ if self._initialized:
46
+ return
47
+ async with aiosqlite.connect(self.db_path) as conn:
48
+ await conn.execute("PRAGMA journal_mode=WAL")
49
+ await conn.executescript(_SCHEMA)
50
+ await conn.commit()
51
+ self._initialized = True
52
+
53
+ async def get(self, key: str, ttl: timedelta) -> bytes | None:
54
+ await self._ensure_init()
55
+ cutoff = time.time() - ttl.total_seconds()
56
+ async with aiosqlite.connect(self.db_path) as conn:
57
+ async with conn.execute(
58
+ "SELECT payload FROM http_cache WHERE cache_key = ? AND cached_at >= ?",
59
+ (key, cutoff),
60
+ ) as cur:
61
+ row = await cur.fetchone()
62
+ return row[0] if row else None
63
+
64
+ async def set(self, key: str, value: bytes, kind: CacheKind) -> None:
65
+ await self._ensure_init()
66
+ async with aiosqlite.connect(self.db_path) as conn:
67
+ await conn.execute(
68
+ """
69
+ INSERT INTO http_cache (cache_key, payload, cached_at, kind)
70
+ VALUES (?, ?, ?, ?)
71
+ ON CONFLICT(cache_key) DO UPDATE SET
72
+ payload = excluded.payload,
73
+ cached_at = excluded.cached_at,
74
+ kind = excluded.kind
75
+ """,
76
+ (key, value, time.time(), kind),
77
+ )
78
+ await conn.commit()
79
+
80
+ async def clear(self, kind: CacheKind | None = None) -> None:
81
+ await self._ensure_init()
82
+ async with aiosqlite.connect(self.db_path) as conn:
83
+ if kind:
84
+ await conn.execute("DELETE FROM http_cache WHERE kind = ?", (kind,))
85
+ else:
86
+ await conn.execute("DELETE FROM http_cache")
87
+ await conn.commit()