MCP TIAGO Dados Abertos — ONS
Servidor MCP que dá a um modelo de linguagem acesso aos dados abertos do ONS, o Operador Nacional do Sistema Elétrico.
Uma camada semântica em YAML (padrão ODCS) declara schema, unidades e agregações válidas de cada dataset. O servidor roteia, valida e executa; o modelo do cliente escreve o SQL. Cada resposta traz unidade e fonte provadas pelo contrato; quando não consegue provar, declara desconhecido em vez de deduzir.
A camada semântica reduz erros do modelo: entrega schema exato, tipos corretos e padrões SQL validados. Nenhum modelo de linguagem roda no servidor: ele entrega o catálogo e os contratos, valida e executa o SQL, e tudo o que devolve é determinístico e rastreável ao contrato.
O que dá errado sem a camada semântica
Para quem analisa o setor elétrico com um assistente de IA e precisa que o número esteja certo, não apenas plausível.
Pedir a carga do Sudeste em agosto a um modelo com acesso aos dados abertos falha em lugares que não são o SQL:
- Escolher o dataset errado. O portal publica recortes parecidos com granularidades diferentes: horário e diário, por subsistema e por usina. A consulta roda, devolve um número plausível, e responde outra pergunta. O catálogo declara granularidade, perspectiva e uso de cada dataset, para a escolha ser pelo grão e não pelo nome.
- Escrever SQL contra um schema imaginado. Coluna que não existe, tipo que não bate, data em texto comparada como texto. O contrato entrega o schema exato, os casts obrigatórios e padrões de SQL já validados antes de o modelo escrever a consulta.
- Errar a unidade. MWmed não é MWh, e a diferença não aparece no resultado. Cada coluna devolvida vem com a unidade que o contrato prova.
- Agregar o que não se agrega. Somar coluna que já é média, ou totalizar linhas que já são um total, dobra o valor sem erro nenhum. O contrato declara a aditividade de cada coluna, e ela acompanha a resposta.
- Perder a procedência. Número em relatório sem saber de onde veio. Cada resposta cita o dataset lido e a URL dele no portal.
A garantia não é que o modelo acerte o SQL: é que o servidor só afirma o que o contrato sustenta. Quando não sustenta, marca o campo como desconhecido em vez de deduzir.
O que o servidor devolve
O modelo escreve a pergunta; o servidor devolve dado, unidade e fonte, provados pelo contrato.
Pergunta: Qual foi a carga do Sudeste em agosto de 2020?
SELECT id_subsistema, ROUND(AVG(TRY_CAST(val_cargaenergiamwmed AS DOUBLE)), 1) AS carga_mwmed
FROM read_parquet('s3://ons-aws-prod-opendata/dataset/carga_energia_di/*.parquet')
WHERE id_subsistema = 'SE' AND din_instante >= '2020-08-01' AND din_instante < '2020-09-01'
GROUP BY id_subsistema
| id_subsistema | carga_mwmed |
|---|---|
| SE | 34682.8 |
{"columns": [{"name": "carga_mwmed", "agg": "AVG", "sources": ["val_cargaenergiamwmed"],
"quantity_kind": "average_power", "unit": "MWmed", "additive": false}]}
{"sources": [{"dataset": "carga-energia", "portal_url": "https://dados.ons.org.br/dataset/carga-energia"}]}
Os exemplos usam 2020 de propósito: a fonte do ONS passa por consistência recorrente e revisa períodos recentes, então um recorte antigo é o que se mantém estável. O que o servidor garante em qualquer período é a forma da resposta: a unidade, a aditividade e a fonte, que vêm do contrato.
A unidade MWmed e a fonte carga-energia não vêm do modelo, vêm do contrato. Se o contrato não declarasse, os blocos não apareceriam. Os blocos acima mostram os campos principais; o result-schema real traz ainda role, mode e unit_tier, e uma entrada para cada coluna do resultado, inclusive as de dimensão.
Pergunta: Qual era a energia armazenada por subsistema em agosto de 2020?
SELECT nom_subsistema, ROUND(AVG(TRY_CAST(ear_verif_subsistema_mwmes AS DOUBLE)), 0) AS ear_mwmes
FROM read_parquet('s3://ons-aws-prod-opendata/dataset/ear_subsistema_di/*.parquet')
WHERE ear_data >= '2020-08-01' AND ear_data < '2020-09-01'
GROUP BY nom_subsistema
| nom_subsistema | ear_mwmes |
|---|---|
| SUDESTE | 91461 |
| NORDESTE | 40821 |
| SUL | 11687 |
| NORTE | 11346 |
{"columns": [{"name": "ear_mwmes", "agg": "AVG", "sources": ["ear_verif_subsistema_mwmes"],
"quantity_kind": "energy_stock", "unit": "MWmes", "additive": false}]}
{"sources": [{"dataset": "ear-diario-por-subsistema", "portal_url": "https://dados.ons.org.br/dataset/ear-diario-por-subsistema"}]}
O additive: false avisa que estes valores não se somam ao longo do tempo: energia armazenada é estoque, uma foto do reservatório naquele dia. Somar o que havia em agosto com o que havia em setembro contaria a mesma água duas vezes. Somar os quatro subsistemas no mesmo dia é outra coisa, e é válido: dá o armazenamento do SIN.
Por que este servidor e não consulta direta?
| Aspecto | Sem o servidor | Com o servidor |
|---|---|---|
| Preparação | Baixar CSV, tratar em pandas/Excel, converter tipos | Pergunta direto; o servidor lê o S3 público |
| Escolha do dataset | O modelo adivinha pelo nome | Catálogo com granularidade, perspectiva e uso |
| Schema | O modelo inventa colunas | Contrato entrega o schema exato |
| Tipos | Cast errado falha em silêncio | TRY_CAST obrigatório, declarado no contrato |
| Unidade | Não se sabe se é MWh ou MWmed | Unidade provada pelo contrato |
| Dupla contagem | Somar SIN e subsistemas dobra o valor | Contrato avisa sobre a linha agregada |
| Procedência | Sem rastro | Cada resposta cita o dataset e a URL |
| Segurança | SQL arbitrário | Validador falha fechado, só leitura |
Nota: O servidor entrega o contexto certo (schema, métricas, fonte). A qualidade do SQL e da interpretação depende do modelo do cliente. LLMs ainda erram; o servidor reduz a superfície de erro, não a elimina.
Como funciona
O contrato de cada dataset declara também granularidade, casts obrigatórios e as perguntas que aquele dataset responde. É a única fonte de verdade: não existe configuração paralela, e tudo o que o servidor afirma sai de um campo do contrato.
flowchart LR
C["Cliente MCP<br/>(o modelo raciocina aqui)"] -->|lê o catálogo| L["listar_datasets<br/>catálogo inteiro<br/>(também resource)"]
C -.->|filtro por termo| B["buscar_dataset<br/>ranking dos contratos"]
L --> D["descrever_dataset<br/>schema, casts, padrões SQL"]
B -.-> D
D -->|o modelo escreve o SQL| E["executar_sql<br/>validador falha fechado"]
E --> K["DuckDB<br/>zero ETL"]
K <-->|Parquet| S3[("S3 público<br/>do ONS")]
Ct[("contracts/<br/>ODCS")] -.-> L & B & D & E
- Roteamento pelo catálogo.
listar_datasetsentrega o catálogo inteiro, uma linha por dataset com granularidade, perspectiva e uso, e o modelo escolhe pelo assunto e pelo grão.buscar_datasetfiltra por termo, com palavra-chave e BM25, para catálogo grande.descrever_datasetmostra o schema, as unidades e os avisos que o modelo precisa antes de escrever SQL. - Execução segura.
executar_sqlaceita só leitura, só das origens do ONS, e devolve o resultado com a unidade e a aditividade de cada coluna provadas pelo contrato. - Só afirma o que prova. Quando o contrato não sustenta uma unidade ou uma
agregação, o servidor devolve
unit: nulleunit_tier: "unknown"em vez de deduzir um valor plausível. Fonte sem URL canônica no contrato é omitida do bloco de proveniência. Abstenção declarada, não erro. - Sem cópia dos dados. O DuckDB lê o bucket público do ONS no momento da consulta, de forma anônima.
Cada resposta de executar_sql é a tabela em markdown seguida de dois blocos JSON:
result-schema, com a unidade e a aditividade de cada coluna e o tier do contrato que as
provou; e tiago-dados-abertos-sources, com o dataset lido e a URL no portal. Bloco que o
contrato não sustenta não aparece.
Usar sem instalar
O servidor está disponível publicamente em:
https://mcp.dados.tiago.ons.org.br/
Veja instruções para cada cliente em dados.ons.org.br/mcp.
Instalação
Requisitos: Python 3.11 ou mais recente e acesso à internet. O bucket do ONS é público e lido anonimamente. O pacote não está no PyPI; a instalação é por clone.
git clone https://github.com/ONSBR/TIAGO-Dados-Abertos.git
cd TIAGO-Dados-Abertos
python -m venv .venv
# Linux/macOS
source .venv/bin/activate
# Windows
.\.venv\Scripts\activate
pip install -e ".[dev]"
python scripts/build_contracts_cache.py # acelera o boot; opcional
Rodar
# STDIO (para IDEs e Claude Desktop)
mcp-tiago-dados-abertos --stdio
# HTTP
mcp-tiago-dados-abertos
# MCP: http://localhost:8003/mcp
# health: http://localhost:8003/health
# catálogo: http://localhost:8003/datasets
Docker
docker build -t mcp-tiago-dados-abertos .
docker run -p 8000:8000 mcp-tiago-dados-abertos
Os contratos vão embutidos na imagem e o cache é gerado no build. A porta é 8003 no run
local e 8000 no container: o endpoint MCP do container fica em http://localhost:8000/mcp
e o health em http://localhost:8000/health. A imagem traz um HEALTHCHECK que consulta
esse endpoint a cada 30 segundos.
Configurar no cliente MCP
Troque <ABS_PATH> pelo caminho absoluto do repositório. Em Linux/macOS o comando é
<ABS_PATH>/.venv/bin/mcp-tiago-dados-abertos. O arquivo
examples/mcp.json tem o bloco pronto para copiar.
| cliente | onde | forma |
|---|---|---|
| Claude Desktop | claude_desktop_config.json |
mcpServers com command e args |
| Claude Code | terminal | claude mcp add tiago-dados-abertos -- <ABS_PATH>/.venv/Scripts/mcp-tiago-dados-abertos.exe --stdio |
| Cursor | .cursor/mcp.json |
mcpServers com command e args |
| Kiro | .kiro/settings/mcp.json |
mcpServers com command e args |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
mcpServers com command e args |
| Gemini CLI | ~/.gemini/settings.json |
mcpServers com command e args |
| VS Code | .vscode/mcp.json |
servers com "type": "stdio", command e args |
Para os clientes com mcpServers:
{
"mcpServers": {
"tiago-dados-abertos": {
"command": "<ABS_PATH>/.venv/Scripts/mcp-tiago-dados-abertos.exe",
"args": ["--stdio"]
}
}
}
Para o VS Code:
{
"servers": {
"tiago-dados-abertos": {
"type": "stdio",
"command": "<ABS_PATH>/.venv/Scripts/mcp-tiago-dados-abertos.exe",
"args": ["--stdio"]
}
}
}
Com o servidor em modo HTTP ou no container, Cursor, Kiro, VS Code e Claude Code aceitam a
URL no lugar de command e args: "url": "http://localhost:8000/mcp" (porta 8003 fora do
container), no VS Code com "type": "http", e no Claude Code
claude mcp add --transport http tiago-dados-abertos http://localhost:8000/mcp.
Tools
| tool | o que faz |
|---|---|
listar_datasets |
o catálogo inteiro, um dataset por linha, com granularidade, perspectiva temporal e uso; a entrada padrão do fluxo |
buscar_dataset |
filtra o catálogo por termo, com palavra-chave e BM25; para catálogo grande ou termo específico |
descrever_dataset |
schema, métricas, unidades, avisos de tipo, patterns SQL e exemplos |
executar_sql |
executa SQL DuckDB somente leitura, paginado, com unidade, fatos e fonte provados |
O conteúdo de listar_datasets também é o resource catalogo://datasets, para clientes que
fixam resources no contexto.
Fluxo recomendado:
1. listar_datasets() → lê o catálogo e escolhe pelo assunto e pelo grão
2. descrever_dataset("carga-energia") → lê schema, unidades e avisos
3. executar_sql("SELECT ...") → executa com as colunas reais
Nunca gere SQL sem ler o schema antes. As colunas têm os nomes do ONS, não os que parecem óbvios, e várias métricas exigem o cast declarado no contrato.
Configuração
Tudo é variável de ambiente. Defina no ambiente do processo, no bloco env do cliente MCP,
ou num arquivo .env na raiz do repositório, que o servidor carrega no boot. Variável já
definida no ambiente vence a do arquivo. Nenhuma é obrigatória. Há um
.env.example com todas comentadas.
Servidor
| variável | padrão | efeito |
|---|---|---|
MCP_HOST |
0.0.0.0 |
endereço de bind no modo HTTP |
MCP_PORT |
8003 |
porta no modo HTTP (o Dockerfile fixa 8000) |
MCP_STATELESS |
true |
sessões HTTP sem estado; deixe true atrás de balanceador |
MCP_ENV_FILE |
.env na raiz do repositório |
caminho do arquivo .env; num pacote instalado, aponte explicitamente |
MCP_CONTRACTS_DIR |
os contratos embutidos no pacote | só para apontar outro diretório de contratos; um contracts_cache.json válido na raiz dele substitui a leitura dos YAML |
Execução de SQL
| variável | padrão | efeito |
|---|---|---|
MCP_QUERY_TIMEOUT_S |
30 |
segundos até o servidor interromper a consulta e sugerir reduzir o escopo |
MCP_DUCKDB_MEMORY_LIMIT |
2GB |
teto de memória do DuckDB |
MCP_MAX_PAGE_LIMIT |
1000 |
maior limit aceito em executar_sql; pedidos acima são reduzidos |
MCP_MAX_SQL_CHARS |
65536 |
maior SQL aceito em executar_sql, em caracteres; acima disso a consulta é recusada antes de qualquer análise |
MCP_HTTP_UA |
mcp-tiago-dados-abertos/1.0 (contato@exemplo.com) |
User-Agent das leituras HTTPS. Troque o contato antes de expor o servidor: é como o ONS identifica quem consulta |
A conexão com o DuckDB é anônima de propósito e não recebe credencial AWS. O bucket do ONS
é público, e a ausência de chave é o que impede um SQL injetado de ler um bucket privado da
sua conta. Não acrescente credencial. A região é fixa em us-west-2, onde o bucket do ONS
vive.
Limites de abuso
| variável | padrão | efeito |
|---|---|---|
MCP_RATE_LIMIT_GLOBAL_MAX |
600 |
consultas por minuto no processo inteiro, somando todos os clientes |
MCP_RATE_LIMIT_MAX_KEYS |
20000 |
teto de chaves distintas no limitador, contra crescimento de memória |
O limite é do processo, mais uma rajada fixa de 20 consultas por segundo. Não há limite por cliente: o servidor não distingue quem chama. Se você expuser o modo HTTP para além da sua rede, ponha um limite por cliente no proxy ou gateway à frente.
Datas e diagnóstico
| variável | padrão | efeito |
|---|---|---|
MCP_DATA_TZ |
America/Sao_Paulo |
fuso em que "hoje", "ontem" e "mês passado" são resolvidos. É o fuso dos dados, não do servidor, e aparece no rodapé de contexto temporal de toda resposta |
MCP_CORR_HEADER |
x-request-id |
cabeçalho HTTP de onde vem o id de correlação da requisição; atrás de um proxy que gera o próprio id, aponte para o cabeçalho dele |
MCP_COBERTURA_PROBE |
vazio | 1 faz descrever_dataset consultar o S3 para descobrir a data mais recente de uma série ativa, com cache de uma hora por dataset e desistência em 8 segundos. Desligado por padrão |
Estrutura do repositório
TIAGO-Dados-Abertos/
├── src/mcp_tiago_dados_abertos/
│ ├── tools/ # 4 tools MCP (buscar, listar, descrever, executar_sql)
│ ├── catalogo/ # carrega contratos, BM25, ranking
│ ├── execucao/ # validador SQL, poda de glob por ano, DuckDB, rate limiter
│ ├── resposta/ # result-schema, aditividade, proveniência, aviso de unidade, rodapé temporal
│ ├── infra/ # config, telemetria, correlação
│ └── server.py # entrypoint MCP (tools + resource catalogo://datasets)
├── contracts/ons/ # contratos ODCS (camada semântica), um por dataset
├── tests/ # catalogo, execucao, tools e corpus (integridade dos contratos)
├── scripts/ # build_contracts_cache.py
├── docs/ # ROUTING.md e assets do README
└── examples/ # mcp.json de exemplo
Contribuir e reportar
Como contribuir com contratos ou com o código: CONTRIBUTING.md. Camada semântica e estrutura dos contratos: CONTRACTS.md. Algoritmo de roteamento e calibração: docs/ROUTING.md. Vulnerabilidade: SECURITY.md. Mudanças entre versões: CHANGELOG.md.
Licença
O código é Apache 2.0: LICENSE. Os dados são do ONS, sob CC-BY, e seguem os termos do Portal de Dados Abertos do ONS; ver NOTICE. Este projeto não redistribui dados: ele lê a origem oficial no momento da consulta.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_tiago_dados_abertos-1.0.1.tar.gz.
File metadata
- Download URL: mcp_tiago_dados_abertos-1.0.1.tar.gz
- Upload date:
- Size: 680.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91252f703563e6d137ba3d75ad6aef0a8939d69f8d26b13416c039e1c8bdd9d8
|
|
| MD5 |
b92bf9c3e46fee0367cb89b8b801b817
|
|
| BLAKE2b-256 |
647db6790e95855aa77e6f8967789a409ccf9f27f3de693cc9ce10b89efa69b8
|
Provenance
The following attestation bundles were made for mcp_tiago_dados_abertos-1.0.1.tar.gz:
Publisher:
publish-pypi.yml on ONSBR/TIAGO-Dados-Abertos
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_tiago_dados_abertos-1.0.1.tar.gz -
Subject digest:
91252f703563e6d137ba3d75ad6aef0a8939d69f8d26b13416c039e1c8bdd9d8 - Sigstore transparency entry: 2757471118
- Sigstore integration time:
-
Permalink:
ONSBR/TIAGO-Dados-Abertos@4a9cfe60c01f452599f7bc90e4728ad3c89335da -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/ONSBR
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@4a9cfe60c01f452599f7bc90e4728ad3c89335da -
Trigger Event:
push
-
Statement type:
File details
Details for the file mcp_tiago_dados_abertos-1.0.1-py3-none-any.whl.
File metadata
- Download URL: mcp_tiago_dados_abertos-1.0.1-py3-none-any.whl
- Upload date:
- Size: 543.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0bf638608dd3a433cdf9e4100240bb261317e9e9b27c7aa59f4aafb4a98a657f
|
|
| MD5 |
e1289d89d383f6709a91129211c7b460
|
|
| BLAKE2b-256 |
abd9521f1b9b44c36e32e8c53ad497db6806746ca5035597c460e31aa3f7d2ac
|
Provenance
The following attestation bundles were made for mcp_tiago_dados_abertos-1.0.1-py3-none-any.whl:
Publisher:
publish-pypi.yml on ONSBR/TIAGO-Dados-Abertos
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_tiago_dados_abertos-1.0.1-py3-none-any.whl -
Subject digest:
0bf638608dd3a433cdf9e4100240bb261317e9e9b27c7aa59f4aafb4a98a657f - Sigstore transparency entry: 2757471206
- Sigstore integration time:
-
Permalink:
ONSBR/TIAGO-Dados-Abertos@4a9cfe60c01f452599f7bc90e4728ad3c89335da -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/ONSBR
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@4a9cfe60c01f452599f7bc90e4728ad3c89335da -
Trigger Event:
push
-
Statement type: