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.
- bcb_mcp-0.1.1/.gitignore +20 -0
- bcb_mcp-0.1.1/CHANGELOG.md +49 -0
- bcb_mcp-0.1.1/LICENSE +21 -0
- bcb_mcp-0.1.1/PKG-INFO +297 -0
- bcb_mcp-0.1.1/README.md +260 -0
- bcb_mcp-0.1.1/pyproject.toml +96 -0
- bcb_mcp-0.1.1/src/bcb_mcp/__init__.py +13 -0
- bcb_mcp-0.1.1/src/bcb_mcp/bcb_client.py +293 -0
- bcb_mcp-0.1.1/src/bcb_mcp/cache.py +633 -0
- bcb_mcp-0.1.1/src/bcb_mcp/config.py +63 -0
- bcb_mcp-0.1.1/src/bcb_mcp/data/series_dictionary.json +2260 -0
- bcb_mcp-0.1.1/src/bcb_mcp/dictionary.py +266 -0
- bcb_mcp-0.1.1/src/bcb_mcp/py.typed +0 -0
- bcb_mcp-0.1.1/src/bcb_mcp/resources.py +91 -0
- bcb_mcp-0.1.1/src/bcb_mcp/server.py +184 -0
- bcb_mcp-0.1.1/src/bcb_mcp/service.py +611 -0
- bcb_mcp-0.1.1/src/bcb_mcp/tools.py +255 -0
- bcb_mcp-0.1.1/tests/conftest.py +92 -0
- bcb_mcp-0.1.1/tests/test_cache.py +233 -0
- bcb_mcp-0.1.1/tests/test_client.py +189 -0
- bcb_mcp-0.1.1/tests/test_dictionary.py +135 -0
- bcb_mcp-0.1.1/tests/test_server.py +98 -0
- bcb_mcp-0.1.1/tests/test_service.py +238 -0
bcb_mcp-0.1.1/.gitignore
ADDED
|
@@ -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).
|
bcb_mcp-0.1.1/README.md
ADDED
|
@@ -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).
|