bcb-mcp 0.1.1__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.
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .venv/
5
+ venv/
6
+ env/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .mypy_cache/
13
+ .coverage
14
+ htmlcov/
15
+ .env
16
+ bcb_cache/
17
+ *.parquet
18
+ *.parquet.tmp
19
+ *.duckdb
20
+ .DS_Store
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 (2026-11-10)
4
+
5
+ Fixes found while reviewing the first release.
6
+
7
+ - `BCB_MCP_TIMEOUT` and `BCB_MCP_MAX_CONCURRENCY` are now actually applied (the
8
+ client was hard-coding 60s and 4 concurrent requests).
9
+ - A `last_date` in the future is clamped to today, so a request such as
10
+ `last_date=2026-12-31` can no longer mark future dates as covered and freeze
11
+ the series until that date.
12
+ - Requests that reach the present revalidate the last few days once the cached
13
+ copy is old enough (`BCB_MCP_TAIL_REFRESH_DAYS` / `..._MINUTES`), picking up new
14
+ observations and BCB revisions.
15
+ - Cache writes take a cross-process file lock and use unique temporary files, so
16
+ several MCP clients can share `~/.cache/bcb-mcp` safely.
17
+ - `get_series` rows are compact (`ref_date`, `value`, `series_id`); metadata is
18
+ returned once per series. The default `max_rows` drops from 5,000 to 1,000
19
+ (a 5-year daily series was ~66k tokens of JSON).
20
+ - `format_data="wide"` disambiguates duplicate series names instead of silently
21
+ overwriting a column, and reports a `warnings` entry.
22
+ - Unexpected exceptions are no longer converted into per-series "failures";
23
+ only `BCBError` is isolated.
24
+ - `query_cache` strips string literals before checking keywords, so aliases such
25
+ as `"set time"` work, and the blocklist now covers `read_text`, `read_csv`,
26
+ `read_parquet`, `glob`, `SET`, and other file/system functions.
27
+ - Dictionary 1.1.0: 151 series, adding IPCA accumulated in 12 months (13522),
28
+ core IPCA EX0 (11427) and smoothed trimmed-mean core (4466), plus Portuguese
29
+ aliases such as `dívida pública` and `taxa de câmbio`. Multi-word queries now
30
+ ignore function words.
31
+
32
+ ## 0.1.0 (2026-11-10)
33
+
34
+ - First release.
35
+ - MCP server for the BCB-SGS time series API (STDIO, Streamable HTTP and SSE).
36
+ - Curated dictionary of ~150 Brazilian macro series with Portuguese and English
37
+ aliases, categories, units, frequencies and descriptions.
38
+ - `search_series` searches the dictionary offline and, optionally, the live BCB
39
+ catalog (paginated, session-aware).
40
+ - `get_series` mirrors `GetBCBData::gbcbd_get_series`: multiple ids, long/wide
41
+ output, chunked downloads for spans longer than the 10-year API limit,
42
+ retries with exponential backoff, and per-series failure reporting.
43
+ - Parquet cache with coverage tracking (only missing ranges are downloaded),
44
+ one parquet file per executed query, and cached catalog searches.
45
+ - `query_cache` runs read-only DuckDB SQL over `cached_series`,
46
+ `cached_queries` and `series_metadata`.
47
+ - `get_series_stats` returns compact summaries for LLM-friendly answers.
48
+ - MCP resources for the dictionary and cache status, plus `macro_briefing` and
49
+ `compare_series` prompts.
bcb_mcp-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcelo Perlin
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.
bcb_mcp-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,297 @@
1
+ Metadata-Version: 2.5
2
+ Name: bcb-mcp
3
+ Version: 0.1.1
4
+ Summary: MCP server for Brazilian Central Bank (BCB-SGS) time series, with a curated series dictionary and a parquet/DuckDB cache
5
+ Project-URL: Homepage, https://github.com/msperlin/bcb-mcp
6
+ Project-URL: Repository, https://github.com/msperlin/bcb-mcp
7
+ Project-URL: Issues, https://github.com/msperlin/bcb-mcp/issues
8
+ Project-URL: Changelog, https://github.com/msperlin/bcb-mcp/blob/main/CHANGELOG.md
9
+ Project-URL: R package, https://github.com/msperlin/GetBCBData
10
+ Author-email: Marcelo Perlin <marceloperlin@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: banco-central,bcb,brazil,duckdb,economics,macro,mcp,model-context-protocol,parquet,sgs,time-series
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Financial
24
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: duckdb<3,>=1.0
28
+ Requires-Dist: fastmcp<5,>=3.4
29
+ Requires-Dist: httpx<1,>=0.27
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
32
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: respx>=0.22; extra == 'dev'
35
+ Requires-Dist: ruff>=0.9; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # bcb-mcp
39
+
40
+ **MCP server for Brazilian Central Bank (BCB-SGS) time series** — the Python counterpart of the R package [`GetBCBData`](https://github.com/msperlin/GetBCBData).
41
+
42
+ It exposes the Central Bank of Brazil's SGS database (Sistema Gerenciador de Séries Temporais) to any MCP client (Claude Desktop, Cursor, Zed, VS Code, OpenCode, …) with three things the raw API does not give you:
43
+
44
+ 1. **A curated dictionary** of ~150 key Brazilian macro series (inflation, rates, activity, labor, external sector, fiscal, credit, monetary, energy, commodities) with Portuguese **and** English aliases, so an LLM can go from "unemployment" or "desemprego" to SGS `24369` without guessing.
45
+ 2. **A parquet cache** — every query is stored as a parquet file and served from disk on the next call, with only the missing date ranges downloaded.
46
+ 3. **DuckDB SQL** over everything cached, so the model can join, aggregate and compare series locally.
47
+
48
+ Like `GetBCBData::gbcbd_get_series()`, it accepts vectors of ids, returns long or wide data, splits long histories into API-friendly chunks (the BCB API rejects requests spanning more than ~10 years), retries failed calls, and reports per-series failures instead of aborting.
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install bcb-mcp
56
+ # or
57
+ uvx bcb-mcp --stdio
58
+ ```
59
+
60
+ > **Note:** if PyPI returns "not found", the release is not published yet — use
61
+ > the source install below (or `uvx --from git+https://github.com/msperlin/bcb-mcp bcb-mcp`).
62
+
63
+ From source:
64
+
65
+ ```bash
66
+ git clone https://github.com/msperlin/bcb-mcp
67
+ cd bcb-mcp
68
+ uv venv && uv pip install -e .
69
+ ```
70
+
71
+ ## Quick start
72
+
73
+ ### As an MCP server (STDIO)
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "bcb": {
79
+ "command": "uvx",
80
+ "args": ["bcb-mcp", "--stdio"]
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### As an HTTP server
87
+
88
+ ```bash
89
+ bcb-mcp --http --host 127.0.0.1 --port 8000 --path /mcp
90
+ ```
91
+
92
+ ### Directly in Python
93
+
94
+ ```python
95
+ import asyncio
96
+ from bcb_mcp import SeriesService
97
+
98
+
99
+ async def main():
100
+ svc = SeriesService()
101
+ df = await svc.get_series(["selic", 433], first_date="2020-01-01", format_data="wide")
102
+ print(df["columns"], df["row_count"])
103
+ await svc.close()
104
+
105
+
106
+ asyncio.run(main())
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Tools
112
+
113
+ | Tool | What it does |
114
+ | --- | --- |
115
+ | `search_series` | Natural language → SGS ids. Searches the bundled dictionary and (optionally) the live BCB catalog. |
116
+ | `list_categories` | Dictionary categories with descriptions and example ids. |
117
+ | `list_series` | Browse the dictionary, optionally by category. |
118
+ | `describe_series` | Name, unit, frequency, source, description, aliases and cache state for given ids. |
119
+ | `get_series` | Download observations. Long/wide, chunked, cached, per-series failure reporting. |
120
+ | `get_series_stats` | Compact summary (last value, mean, min, max, change) without the raw rows. |
121
+ | `query_cache` | Read-only DuckDB SQL over all cached parquet files. |
122
+ | `cache_status` | What is cached: series, coverage ranges, rows, disk usage. |
123
+ | `clear_cache` | Evict selected series or the whole cache. |
124
+ | `today` | Server date plus handy "one/five years ago" dates. |
125
+
126
+ ### Output shape
127
+
128
+ `get_series` returns one compact row per observation (`ref_date`, `value`,
129
+ `series_id`). The name, unit, frequency and source of each series are returned
130
+ once in the `series` array, and `failures` lists the ids that could not be
131
+ fetched. Payload size matters for LLMs: the default `max_rows` is **1,000 per
132
+ series** (about 50k tokens of JSON), so use `get_series_stats` when you only need
133
+ the numbers, `format_data="wide"` to align series on dates, or raise `max_rows`
134
+ for the full history.
135
+
136
+ Cache freshness: a range is downloaded once, but requests that reach the present
137
+ revalidate the last `BCB_MCP_TAIL_REFRESH_DAYS` days at most every
138
+ `BCB_MCP_TAIL_REFRESH_MINUTES` minutes, so new observations and BCB revisions
139
+ show up without refetching on every call.
140
+
141
+ ### Resources
142
+
143
+ - `bcb://dictionary/series` — the full curated dictionary (JSON).
144
+ - `bcb://dictionary/categories` — categories and counts.
145
+ - `bcb://cache/status` — local cache summary.
146
+
147
+ ### Prompts
148
+
149
+ - `macro_briefing` — guided workflow for a short Brazilian macro briefing.
150
+ - `compare_series` — guided workflow to compare series over time.
151
+
152
+ ---
153
+
154
+ ## Example prompts
155
+
156
+ > "What is the latest Selic rate and the 12-month IPCA? Compare them since 2015."
157
+
158
+ > "Puxe a taxa de desemprego (PNAD Contínua) e o IBC-Br dos últimos 3 anos e diga se a atividade está acelerando."
159
+
160
+ > "Compare credit outstanding as a share of GDP with the average lending rate since 2015. Use SQL if needed."
161
+
162
+ The model will call `search_series("selic")` → `get_series_stats([432, 13522])` →
163
+ `get_series([432, 13522], format_data="wide")`, and can fall back to `query_cache`
164
+ for cross-series SQL. Note the units: `432` is the Selic target in `% p.y.`
165
+ (comparable to annual inflation), while `11` is the daily Selic in `% p.d.`.
166
+
167
+ ---
168
+
169
+ ## The dictionary
170
+
171
+ `src/bcb_mcp/data/series_dictionary.json` ships 151 hand-picked series, each with:
172
+
173
+ ```json
174
+ {
175
+ "id": 24369,
176
+ "name": "Unemployment rate - PNADC",
177
+ "name_pt": "Taxa de desocupacao - PNAD Continua",
178
+ "category": "labor",
179
+ "unit": "%",
180
+ "frequency": "Monthly",
181
+ "source": "IBGE",
182
+ "description": "Unemployment rate from the Continuous PNAD survey.",
183
+ "aliases": ["desemprego", "taxa de desemprego", "unemployment", "unemployment rate", "pnadc"]
184
+ }
185
+ ```
186
+
187
+ Search is accent- and case-insensitive (`desemprego`, `Desemprego`, `DESEMPREGO`), matches English and Portuguese, ignores short function words (`da`, `de`, `of`, ...) in multi-word queries, and ranks exact alias hits above substring hits. The catalog includes the headline aggregates an LLM usually asks for first — IPCA in 12 months (`13522`), core IPCA (`11427`/`4466`), gross and net public debt (`13762`/`4513`) — plus Portuguese aliases such as `dívida pública` and `taxa de câmbio`. Anything not in the dictionary can still be found through the live BCB catalog search (English names), and any numeric id can be passed straight to `get_series`.
188
+
189
+ Regenerate the dictionary metadata from the official catalog:
190
+
191
+ ```bash
192
+ python scripts/build_dictionary.py --check
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Cache layout
198
+
199
+ ```
200
+ ~/.cache/bcb-mcp/ # or $BCB_MCP_CACHE_DIR
201
+ ├── parquet/
202
+ │ ├── series/sgs_11.parquet # consolidated, deduplicated series store
203
+ │ ├── queries/sgs_11__2024-01-01__2024-12-31.parquet
204
+ │ └── catalog/search_<hash>.parquet # cached catalog searches
205
+ ├── meta/sgs_11.json # coverage ranges, rows, official metadata
206
+ └── .write.lock # cross-process lock for cache mutations
207
+ ```
208
+
209
+ - Every executed query is written as its own parquet file (auditable, reusable).
210
+ - The consolidated store tracks **coverage intervals**: asking for 2010–2024 after having 2015–2024 downloads only 2010–2014.
211
+ - Writes use a cross-process file lock and unique temporary files, so several
212
+ MCP clients (Claude Desktop, Cursor, OpenCode, …) can share the same cache
213
+ directory safely.
214
+ - Requests that reach the present revalidate the last few days periodically
215
+ (see the freshness settings below); future dates are clamped to today so a
216
+ `last_date` in the future cannot mark data as permanently cached.
217
+ - All I/O goes through DuckDB; no pandas/pyarrow needed.
218
+ - Point the cache elsewhere with `--cache-dir` or `BCB_MCP_CACHE_DIR`.
219
+
220
+ ### Querying the cache
221
+
222
+ ```sql
223
+ SELECT series_name,
224
+ date_trunc('year', ref_date) AS year,
225
+ avg(value) AS avg_value
226
+ FROM cached_series
227
+ WHERE series_id IN (11, 433)
228
+ GROUP BY 1, 2
229
+ ORDER BY 1, 2;
230
+ ```
231
+
232
+ Views available in `query_cache`: `cached_series`, `cached_queries`, `series_metadata`.
233
+
234
+ ---
235
+
236
+ ## Mapping from the R package
237
+
238
+ | GetBCBData | bcb-mcp |
239
+ | --- | --- |
240
+ | `gbcbd_get_series(id, first_date, last_date, format_data)` | `get_series(series_ids, first_date, last_date, format_data)` |
241
+ | `names(id) <- c("Selic" = 11)` | `series_names=["Selic"]` (or just use aliases) |
242
+ | `gbcbd_search_series("selic")` | `search_series("selic")` |
243
+ | `use_memoise = TRUE` | `use_cache=True` (default) — parquet instead of memoise |
244
+ | `cache_path` | `--cache-dir` / `BCB_MCP_CACHE_DIR` |
245
+ | `do_parallel = TRUE` | always concurrent, bounded by `BCB_MCP_MAX_CONCURRENCY` (default 4) |
246
+ | 3-year chunking | 10-year chunking (the API limit), same retry/backoff behaviour |
247
+ | long / wide data frames | long / wide JSON rows |
248
+
249
+ ---
250
+
251
+ ## Configuration
252
+
253
+ | Variable | Default | Meaning |
254
+ | --- | --- | --- |
255
+ | `BCB_MCP_CACHE_DIR` | `~/.cache/bcb-mcp` | Cache directory. |
256
+ | `BCB_MCP_TIMEOUT` | `60` | HTTP timeout (seconds). |
257
+ | `BCB_MCP_MAX_RETRIES` | `3` | Retries per request (exponential backoff). |
258
+ | `BCB_MCP_MIN_INTERVAL` | `0.2` | Minimum seconds between requests. |
259
+ | `BCB_MCP_MAX_CONCURRENCY` | `4` | Concurrent series downloads. |
260
+ | `BCB_MCP_MAX_ROWS` | `1000` | Default observations kept per series. |
261
+ | `BCB_MCP_TAIL_REFRESH_DAYS` | `3` | Days revalidated when a request reaches the present (0 disables). |
262
+ | `BCB_MCP_TAIL_REFRESH_MINUTES` | `60` | How old the cached copy must be before the tail is revalidated. |
263
+
264
+ ---
265
+
266
+ ## Development
267
+
268
+ ```bash
269
+ uv venv && uv pip install -e ".[dev]"
270
+ pytest --cov=bcb_mcp --cov-report=term-missing
271
+ ruff check . && ruff format --check .
272
+ ```
273
+
274
+ Tests mock HTTP with `respx`; no network access is required.
275
+
276
+ ## Releasing to PyPI
277
+
278
+ The package is ready to publish. Two options:
279
+
280
+ 1. **Trusted publishing (recommended, no token).** Create the project on PyPI as a
281
+ *pending publisher* for `bcb-mcp` with workflow `publish.yml` and environment
282
+ `pypi`, then publish a GitHub release. The workflow builds the sdist/wheel and
283
+ uploads them with OIDC.
284
+ 2. **Manual.** `uv build && uv publish` (needs `UV_PUBLISH_TOKEN` or a
285
+ `~/.pypirc`).
286
+
287
+ Before tagging, bump the version in `pyproject.toml` and `src/bcb_mcp/config.py`
288
+ and add a `CHANGELOG.md` entry.
289
+
290
+
291
+ ## Data source and disclaimer
292
+
293
+ Data comes from the public BCB SGS endpoints (`api.bcb.gov.br` and `www3.bcb.gov.br`). Series may be revised or discontinued by the Central Bank. This project is not affiliated with the Banco Central do Brasil. Use at your own risk.
294
+
295
+ ## License
296
+
297
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,260 @@
1
+ # bcb-mcp
2
+
3
+ **MCP server for Brazilian Central Bank (BCB-SGS) time series** — the Python counterpart of the R package [`GetBCBData`](https://github.com/msperlin/GetBCBData).
4
+
5
+ It exposes the Central Bank of Brazil's SGS database (Sistema Gerenciador de Séries Temporais) to any MCP client (Claude Desktop, Cursor, Zed, VS Code, OpenCode, …) with three things the raw API does not give you:
6
+
7
+ 1. **A curated dictionary** of ~150 key Brazilian macro series (inflation, rates, activity, labor, external sector, fiscal, credit, monetary, energy, commodities) with Portuguese **and** English aliases, so an LLM can go from "unemployment" or "desemprego" to SGS `24369` without guessing.
8
+ 2. **A parquet cache** — every query is stored as a parquet file and served from disk on the next call, with only the missing date ranges downloaded.
9
+ 3. **DuckDB SQL** over everything cached, so the model can join, aggregate and compare series locally.
10
+
11
+ Like `GetBCBData::gbcbd_get_series()`, it accepts vectors of ids, returns long or wide data, splits long histories into API-friendly chunks (the BCB API rejects requests spanning more than ~10 years), retries failed calls, and reports per-series failures instead of aborting.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install bcb-mcp
19
+ # or
20
+ uvx bcb-mcp --stdio
21
+ ```
22
+
23
+ > **Note:** if PyPI returns "not found", the release is not published yet — use
24
+ > the source install below (or `uvx --from git+https://github.com/msperlin/bcb-mcp bcb-mcp`).
25
+
26
+ From source:
27
+
28
+ ```bash
29
+ git clone https://github.com/msperlin/bcb-mcp
30
+ cd bcb-mcp
31
+ uv venv && uv pip install -e .
32
+ ```
33
+
34
+ ## Quick start
35
+
36
+ ### As an MCP server (STDIO)
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "bcb": {
42
+ "command": "uvx",
43
+ "args": ["bcb-mcp", "--stdio"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### As an HTTP server
50
+
51
+ ```bash
52
+ bcb-mcp --http --host 127.0.0.1 --port 8000 --path /mcp
53
+ ```
54
+
55
+ ### Directly in Python
56
+
57
+ ```python
58
+ import asyncio
59
+ from bcb_mcp import SeriesService
60
+
61
+
62
+ async def main():
63
+ svc = SeriesService()
64
+ df = await svc.get_series(["selic", 433], first_date="2020-01-01", format_data="wide")
65
+ print(df["columns"], df["row_count"])
66
+ await svc.close()
67
+
68
+
69
+ asyncio.run(main())
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Tools
75
+
76
+ | Tool | What it does |
77
+ | --- | --- |
78
+ | `search_series` | Natural language → SGS ids. Searches the bundled dictionary and (optionally) the live BCB catalog. |
79
+ | `list_categories` | Dictionary categories with descriptions and example ids. |
80
+ | `list_series` | Browse the dictionary, optionally by category. |
81
+ | `describe_series` | Name, unit, frequency, source, description, aliases and cache state for given ids. |
82
+ | `get_series` | Download observations. Long/wide, chunked, cached, per-series failure reporting. |
83
+ | `get_series_stats` | Compact summary (last value, mean, min, max, change) without the raw rows. |
84
+ | `query_cache` | Read-only DuckDB SQL over all cached parquet files. |
85
+ | `cache_status` | What is cached: series, coverage ranges, rows, disk usage. |
86
+ | `clear_cache` | Evict selected series or the whole cache. |
87
+ | `today` | Server date plus handy "one/five years ago" dates. |
88
+
89
+ ### Output shape
90
+
91
+ `get_series` returns one compact row per observation (`ref_date`, `value`,
92
+ `series_id`). The name, unit, frequency and source of each series are returned
93
+ once in the `series` array, and `failures` lists the ids that could not be
94
+ fetched. Payload size matters for LLMs: the default `max_rows` is **1,000 per
95
+ series** (about 50k tokens of JSON), so use `get_series_stats` when you only need
96
+ the numbers, `format_data="wide"` to align series on dates, or raise `max_rows`
97
+ for the full history.
98
+
99
+ Cache freshness: a range is downloaded once, but requests that reach the present
100
+ revalidate the last `BCB_MCP_TAIL_REFRESH_DAYS` days at most every
101
+ `BCB_MCP_TAIL_REFRESH_MINUTES` minutes, so new observations and BCB revisions
102
+ show up without refetching on every call.
103
+
104
+ ### Resources
105
+
106
+ - `bcb://dictionary/series` — the full curated dictionary (JSON).
107
+ - `bcb://dictionary/categories` — categories and counts.
108
+ - `bcb://cache/status` — local cache summary.
109
+
110
+ ### Prompts
111
+
112
+ - `macro_briefing` — guided workflow for a short Brazilian macro briefing.
113
+ - `compare_series` — guided workflow to compare series over time.
114
+
115
+ ---
116
+
117
+ ## Example prompts
118
+
119
+ > "What is the latest Selic rate and the 12-month IPCA? Compare them since 2015."
120
+
121
+ > "Puxe a taxa de desemprego (PNAD Contínua) e o IBC-Br dos últimos 3 anos e diga se a atividade está acelerando."
122
+
123
+ > "Compare credit outstanding as a share of GDP with the average lending rate since 2015. Use SQL if needed."
124
+
125
+ The model will call `search_series("selic")` → `get_series_stats([432, 13522])` →
126
+ `get_series([432, 13522], format_data="wide")`, and can fall back to `query_cache`
127
+ for cross-series SQL. Note the units: `432` is the Selic target in `% p.y.`
128
+ (comparable to annual inflation), while `11` is the daily Selic in `% p.d.`.
129
+
130
+ ---
131
+
132
+ ## The dictionary
133
+
134
+ `src/bcb_mcp/data/series_dictionary.json` ships 151 hand-picked series, each with:
135
+
136
+ ```json
137
+ {
138
+ "id": 24369,
139
+ "name": "Unemployment rate - PNADC",
140
+ "name_pt": "Taxa de desocupacao - PNAD Continua",
141
+ "category": "labor",
142
+ "unit": "%",
143
+ "frequency": "Monthly",
144
+ "source": "IBGE",
145
+ "description": "Unemployment rate from the Continuous PNAD survey.",
146
+ "aliases": ["desemprego", "taxa de desemprego", "unemployment", "unemployment rate", "pnadc"]
147
+ }
148
+ ```
149
+
150
+ Search is accent- and case-insensitive (`desemprego`, `Desemprego`, `DESEMPREGO`), matches English and Portuguese, ignores short function words (`da`, `de`, `of`, ...) in multi-word queries, and ranks exact alias hits above substring hits. The catalog includes the headline aggregates an LLM usually asks for first — IPCA in 12 months (`13522`), core IPCA (`11427`/`4466`), gross and net public debt (`13762`/`4513`) — plus Portuguese aliases such as `dívida pública` and `taxa de câmbio`. Anything not in the dictionary can still be found through the live BCB catalog search (English names), and any numeric id can be passed straight to `get_series`.
151
+
152
+ Regenerate the dictionary metadata from the official catalog:
153
+
154
+ ```bash
155
+ python scripts/build_dictionary.py --check
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Cache layout
161
+
162
+ ```
163
+ ~/.cache/bcb-mcp/ # or $BCB_MCP_CACHE_DIR
164
+ ├── parquet/
165
+ │ ├── series/sgs_11.parquet # consolidated, deduplicated series store
166
+ │ ├── queries/sgs_11__2024-01-01__2024-12-31.parquet
167
+ │ └── catalog/search_<hash>.parquet # cached catalog searches
168
+ ├── meta/sgs_11.json # coverage ranges, rows, official metadata
169
+ └── .write.lock # cross-process lock for cache mutations
170
+ ```
171
+
172
+ - Every executed query is written as its own parquet file (auditable, reusable).
173
+ - The consolidated store tracks **coverage intervals**: asking for 2010–2024 after having 2015–2024 downloads only 2010–2014.
174
+ - Writes use a cross-process file lock and unique temporary files, so several
175
+ MCP clients (Claude Desktop, Cursor, OpenCode, …) can share the same cache
176
+ directory safely.
177
+ - Requests that reach the present revalidate the last few days periodically
178
+ (see the freshness settings below); future dates are clamped to today so a
179
+ `last_date` in the future cannot mark data as permanently cached.
180
+ - All I/O goes through DuckDB; no pandas/pyarrow needed.
181
+ - Point the cache elsewhere with `--cache-dir` or `BCB_MCP_CACHE_DIR`.
182
+
183
+ ### Querying the cache
184
+
185
+ ```sql
186
+ SELECT series_name,
187
+ date_trunc('year', ref_date) AS year,
188
+ avg(value) AS avg_value
189
+ FROM cached_series
190
+ WHERE series_id IN (11, 433)
191
+ GROUP BY 1, 2
192
+ ORDER BY 1, 2;
193
+ ```
194
+
195
+ Views available in `query_cache`: `cached_series`, `cached_queries`, `series_metadata`.
196
+
197
+ ---
198
+
199
+ ## Mapping from the R package
200
+
201
+ | GetBCBData | bcb-mcp |
202
+ | --- | --- |
203
+ | `gbcbd_get_series(id, first_date, last_date, format_data)` | `get_series(series_ids, first_date, last_date, format_data)` |
204
+ | `names(id) <- c("Selic" = 11)` | `series_names=["Selic"]` (or just use aliases) |
205
+ | `gbcbd_search_series("selic")` | `search_series("selic")` |
206
+ | `use_memoise = TRUE` | `use_cache=True` (default) — parquet instead of memoise |
207
+ | `cache_path` | `--cache-dir` / `BCB_MCP_CACHE_DIR` |
208
+ | `do_parallel = TRUE` | always concurrent, bounded by `BCB_MCP_MAX_CONCURRENCY` (default 4) |
209
+ | 3-year chunking | 10-year chunking (the API limit), same retry/backoff behaviour |
210
+ | long / wide data frames | long / wide JSON rows |
211
+
212
+ ---
213
+
214
+ ## Configuration
215
+
216
+ | Variable | Default | Meaning |
217
+ | --- | --- | --- |
218
+ | `BCB_MCP_CACHE_DIR` | `~/.cache/bcb-mcp` | Cache directory. |
219
+ | `BCB_MCP_TIMEOUT` | `60` | HTTP timeout (seconds). |
220
+ | `BCB_MCP_MAX_RETRIES` | `3` | Retries per request (exponential backoff). |
221
+ | `BCB_MCP_MIN_INTERVAL` | `0.2` | Minimum seconds between requests. |
222
+ | `BCB_MCP_MAX_CONCURRENCY` | `4` | Concurrent series downloads. |
223
+ | `BCB_MCP_MAX_ROWS` | `1000` | Default observations kept per series. |
224
+ | `BCB_MCP_TAIL_REFRESH_DAYS` | `3` | Days revalidated when a request reaches the present (0 disables). |
225
+ | `BCB_MCP_TAIL_REFRESH_MINUTES` | `60` | How old the cached copy must be before the tail is revalidated. |
226
+
227
+ ---
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ uv venv && uv pip install -e ".[dev]"
233
+ pytest --cov=bcb_mcp --cov-report=term-missing
234
+ ruff check . && ruff format --check .
235
+ ```
236
+
237
+ Tests mock HTTP with `respx`; no network access is required.
238
+
239
+ ## Releasing to PyPI
240
+
241
+ The package is ready to publish. Two options:
242
+
243
+ 1. **Trusted publishing (recommended, no token).** Create the project on PyPI as a
244
+ *pending publisher* for `bcb-mcp` with workflow `publish.yml` and environment
245
+ `pypi`, then publish a GitHub release. The workflow builds the sdist/wheel and
246
+ uploads them with OIDC.
247
+ 2. **Manual.** `uv build && uv publish` (needs `UV_PUBLISH_TOKEN` or a
248
+ `~/.pypirc`).
249
+
250
+ Before tagging, bump the version in `pyproject.toml` and `src/bcb_mcp/config.py`
251
+ and add a `CHANGELOG.md` entry.
252
+
253
+
254
+ ## Data source and disclaimer
255
+
256
+ Data comes from the public BCB SGS endpoints (`api.bcb.gov.br` and `www3.bcb.gov.br`). Series may be revised or discontinued by the Central Bank. This project is not affiliated with the Banco Central do Brasil. Use at your own risk.
257
+
258
+ ## License
259
+
260
+ MIT — see [LICENSE](LICENSE).