Status: implemented (alpha curated catalog). Non-breaking: the REST API is untouched. Implemented in
src/findata/api/mcp_app.py.
The MCP server used to be auto-generated 1:1 from the FastAPI app:
FastApiMCP(app) turns every route into a tool, so the catalog was 95 tools,
one per dataset/endpoint. From a client/agent's point of view that means:
- ~21k tokens of
tools/listloaded at the start of every session, before a single call. - Worse tool selection, a model picks worse among 95 near-duplicate names (one tool per SGS series, per CVM fund facet…) than among ~two dozen well-described tools.
A separate FastAPI app, mcp_app, is the only source of the tool catalog.
It exposes a small, hand-curated set of tools that dispatch to the same
findata.sources.* functions the REST routers already use.
# app.py: tools come from mcp_app; transport is served on the public app
_mcp = FastApiMCP(mcp_app, name=..., description=...)
_mcp.mount_http(router=app) # /mcp on the public app; REST routes untouchedFastApiMCP(mcp_app) builds the catalog from mcp_app's OpenAPI and executes
each tool via httpx.ASGITransport(app=mcp_app). Because the routers carry no
app-state/rate-limiter coupling, reusing the source functions in a second app is
safe. The 95 REST routes that back the CLI and HTTP consumers never change.
- A, curation. Each tool has an explicit
operation_id, an agent-oriented one-linesummary, and a docstring written for an agent deciding whether to call it, not the raw route docstring.response_model=None+-> Anykeeps response schemas out of the catalog (they would re-inflate it). - B, consolidation. Sprawly clusters collapse behind a
dataset/kindselector (see table). The work moves from "many thin tools" to "few tools with good docs". - C, code mode. One optional tool,
findata_run_code, runs a Python snippet against thefindatalibrary in an isolated child interpreter. It replaces dozens of fine-grained calls for filter/join/aggregate flows that would otherwise stream every intermediate result through the model's context. Gated off by default (FINDATA_MCP_CODE_MODE=1to enable).
| 1:1 (old) | curated (new) | |
|---|---|---|
| MCP tools | 95 | 25 (26 with code mode) |
tools/list size |
~85k chars (~21k tok) | ~30k chars (~7k tok) |
| REST operations | 95 | 96 |
registry_lookup ← start here: CNPJ / ticker / code / name → entities
resolve_asset ← classify an asset: macro asset class + exposure
bcb_series bcb_ptax bcb_focus (BCB: 12 → 3)
cvm_company cvm_financials cvm_fund cvm_structured_fund (CVM: 22 → 4)
b3_quote b3_cotahist b3_index (B3: 9 → 3)
tesouro_bonds tesouro_siconfi (Tesouro: 6 → 2)
ibge_indicator ibge_ipca_breakdown (IBGE: 4 → 2)
ipea_series ipea_search (IPEA: 4 → 2)
anbima (ANBIMA: ima|ettj|debentures|tpf)
openfinance_directory (Open Finance: 15 → 1)
basedosdados_search basedosdados_sql (BdD: 7 → 2)
receita_arrecadacao aneel_leiloes susep_empresas
findata_run_code (code mode, opt-in)
| Tool | Folds in | Selector |
|---|---|---|
bcb_series |
/series, /series/code/{code}, /series/name/{name} |
code / name / none=catalog |
bcb_ptax |
/ptax/usd, /ptax/usd/period, /ptax/{currency} |
start+end → period |
bcb_focus |
/focus/{indicators,annual,monthly,selic,top5} |
horizon, panel, indicator |
cvm_company |
companies search/list, fca/*, ipe |
dataset=search|list|fca_*|filings |
cvm_fund |
funds, funds/{daily,holdings,lamina,profile,periods}, returns |
dataset |
cvm_structured_fund |
funds/{fii,fidc,fip}/* |
kind + dataset |
b3_index |
index portfolio + monthly + list | dataset, omit symbol to list |
tesouro_bonds |
bonds list/search/history | dataset |
tesouro_siconfi |
rreo, rgf, entes |
report |
anbima |
ima, ettj, debentures, tpf | dataset=ima|ettj|debentures|tpf |
openfinance_directory |
participants/endpoints/resources/roles | dataset |
- Fewer but "fatter" tools. Each carries a
datasetenum and more doc. The whole bet is that good descriptions beat tool count, so the docstrings are the deliverable, not an afterthought. - Consolidation can hide endpoint-specific params behind an enum. Mitigated
by documenting each
dataset/kindvalue and validating bad combinations with a400(e.g.cvm_fund dataset=holdingsrequirescnpj+month), matching the REST API'sValueError → 400behaviour. - Discoverability of rare endpoints. A handful of niche REST routes are not
individually surfaced as tools. They remain fully reachable over REST and via
findata_run_code.
findata_run_code is a prototype, not a hardened sandbox. The snippet runs
in a child python -I (isolated mode, cwd in a tempdir) with a wall-clock
timeout and a 20k-char output cap, but it has full library and network access.
It is disabled unless FINDATA_MCP_CODE_MODE=1 and is intended for trusted,
local/agent use. A production deployment should run it in a real sandbox
(container/seccomp/network egress controls) before enabling.
registry_lookup(q="PETR4")→ PETROBRAS, CNPJ33.000.167/0001-01,[PETR3, PETR4](offline).bcb_ptax(start=2024-01-02, end=2024-01-05)→ daily PTAX USD series (the handoff's headline flow).findata_run_code("import findata; ...")→ runs in the sandbox, returns captured stdout.