Servidor MCP do BeryX
O BeryX expõe a qualidade de dados a agentes IA via Model Context Protocol (Claude Desktop, Claude Code, ou qualquer cliente MCP). O servidor é um bridge para a API HTTP — autentica-se como um utilizador normal (workspace e RBAC respeitados) e é read-only sobre as fontes; a única ação com efeitos é pedir um diagnóstico RCA (cria um relatório IA).
Tools disponíveis
| Tool | O que faz |
|---|---|
list_datasets |
Datasets do workspace com linhas, tamanho e score |
get_dataset |
Detalhe: colunas, issues abertos, sugestões |
list_issues |
Issues com filtros (dataset, severidade, estado); os que têm tickets externos ligados trazem tickets (chave + estado) |
list_suggestions |
Sugestões de melhoria (heurísticas + IA) |
get_quality_overview |
Risco, exposição €/mês e totais FinOps |
get_verdict |
Veredito para gates de CI («posso publicar?»): pass/warn/fail/unknown |
get_dataset_context |
Contexto unificado de um dataset numa só chamada (ver abaixo); sections opcional |
get_trust_decision |
Decisão de confiança antes de consumir, por finalidade (agente/rag/treino/bi): trusted/caution/untrusted/unknown, razões, required_actions e ttl_seconds/valid_until — ver TRUST_API.md. Cada chamada fica na auditoria (trust_decision_checked) |
get_trust_facts |
Factos de confiança do dataset num envelope estável (schema_version 1): veredito com política, decisão agente, score e tendência 7d, contrato, política, contagens de sensibilidade, AI-readiness, freshness, ownership, linhagem, issues e links. detail=minimal (omissão, sem nomes de colunas) ou full — ver api/trust-facts.md |
list_domains |
Data products (domínios) com owner, score e attainment do SLA no mês |
get_domain_readiness |
Readiness de um data product por caso de uso (rag/ml/agente/bi): ready/ready_with_warnings/not_ready com bloqueadores e avisos — ver abaixo |
get_domain_verdict |
Veredito de um data product para gates de CI («posso publicar o domínio?»): pass/warn/fail/unknown agregado dos datasets, com counts e o veredito de cada dataset; with_policy (omissão true) conta a política de dados. Mesmo JSON de GET /api/v1/domains/{id}/verdict (cartão #148) |
match_datasets |
Última correspondência de entidades entre dois datasets (ex.: clientes do CRM × clientes do ERP): linhas de cada lado, pares, sem par, ambíguas 1→N, cobertura %, chaves usadas e a sobrevivência proposta (nunca aplicada). other_dataset_id opcional. Só lê o último run concluído, não lança correspondências (pedem-se em Duplicados › Entre datasets) e não devolve valores dos registos. Plano Team+ |
diagnose_issue |
Pede RCA de um issue (devolve report_id) — só em stdio |
search_knowledge |
Pesquisa a base de conhecimento do workspace (resumos de relatórios e RCAs, resoluções de issues, decisões e notas): filtros query, kind e dataset_id; até 10 entradas, sem as obsoletas |
get_report |
Estado/conteúdo Markdown de um relatório IA |
get_failing_rows_sql |
Query SQL que lista as linhas que falham uma regra (rule_id), no dialeto da fonte ou noutro (dialect: postgres, mysql, mssql, bigquery, snowflake, databricks, sqlite), com os nomes citados e um limite (limit, omissão 100). Só devolve o texto: o MCP nunca a executa. Regra sem tradução por linha (ex.: row_count_min): sql a null, com reason e message. Lê GET /api/v1/rules/{id}/failing-rows.sql (cartão #400) |
get_lineage |
Linhagem de um dataset («o que alimenta X?»): upstream e/ou downstream (direction) até hops saltos (1 a 3), cada dataset com o caminho no sentido do fluxo (path: «A → B → C»), as arestas com a origem (kinds: declared, dbt, openlineage, parsed, inferred), as colunas de ligação e o mapa de colunas quando existe, e os exposures a jusante. Lê GET /api/v1/datasets/{id}/lineage/traverse (cartão #173) |
impact_of |
O que parte se mudar um dataset ou uma coluna (column_name opcional): datasets, exposures e destinos fora do warehouse a jusante e, com a coluna, as colunas que derivam dela (columns, só com linhagem de colunas). Datasets e colunas até 3 saltos (max_depth), exposures até 6 (exposure_max_depth), como na análise do impacto de um issue. Lê GET /api/v1/datasets/{id}/lineage/impact |
path_between |
Caminho mais curto entre dois datasets (até 6 saltos): path, path_text («A → B → C») e as arestas; sem caminho de A para B procura de B para A (direction: target_to_source). Lê GET /api/v1/datasets/{id}/lineage/path |
find_datasets |
Pesquisa composta: query, layer (bronze/silver/gold), domain (id ou nome), has_pii, min_score e usage_class (hot/active/cold/dead) aplicados em conjunto; devolve camada, score, classe de uso e as colunas com PII (só nomes). A camada, o PII e o score filtram os primeiros 500 datasets que cumprem o texto, o domínio e a classe de uso; com mais, truncated: true e o total só conta esses 500. Lê GET /api/v1/datasets/find |
As quatro tools de travessia (cartão #173) são determinísticas: não chamam nenhum LLM, por isso
funcionam com BYOK, com a IA desligada e sem chave. O chat do assistente usa as mesmas funções
(backend/app/domain/lineage/traversal.py) e os mesmos resumos. Tetos: 40 datasets e 60 arestas
(truncated: true quando cortam), só nomes — nunca o owner, o email nem o URL de um exposure. Um
dataset de um domínio restrito sem acesso aparece como «Dataset restrito», com um id opaco
(restrito-N) em vez do real e sem colunas. Uma aresta com uma ponta noutro workspace é ignorada.
Resources (read-only, application/json)
| URI | Conteúdo |
|---|---|
quality://overview |
Mesmo resumo de get_quality_overview |
datasets://catalog |
Catálogo de datasets (mesmo formato de list_datasets) |
datasets://{dataset_id} |
Detalhe de um dataset (mesmo formato de get_dataset) |
datasets://{dataset_id}/verdict |
Veredito de CI do dataset |
datasets://{dataset_id}/lineage |
Linhagem direta do dataset (dataset-lineage): o resultado de get_lineage com 1 salto nos dois sentidos (cartão #173) |
datasets://{dataset_id}/context |
Contexto unificado do dataset (mesmo formato de get_dataset_context) |
datasets://{dataset_id}/trust-decision |
Decisão de confiança para a finalidade agente (mesmo JSON da API e de get_trust_decision) |
datasets://{dataset_id}/trust-decision/{purpose} |
Aptidão do dataset no ponto de consumo para purpose (agente/rag/treino/bi; ml = treino): a mesma decisão, com required_actions e valid_until (cartão #151) |
datasets://{dataset_id}/trust-facts |
Factos de confiança do dataset (dataset-trust-facts, detail=minimal): o mesmo JSON da API e de get_trust_facts (cartão #153) |
domains://catalog |
Data products do workspace: id, name, owner, score, attainment_current_month_pct (mesmo formato de list_domains) |
glossary://terms |
Glossário de negócio (glossary, plano Team+): termos com definição, sinónimos, as colunas aprovadas e a qualidade herdada delas — score, open_issues_total e explanation (cartão #169) |
domains://{domain_id}/readiness |
Readiness do data product para bi (mesmo JSON da API e de get_domain_readiness) |
domains://{domain_id}/readiness?use_case=rag |
Readiness para outro caso de uso (rag/ml/agente/bi; treino = ml) |
domains://{domain_id}/verdict |
Veredito de CI do data product, com a política de dados (domain-verdict): o mesmo JSON da API e de get_domain_verdict (cartão #148) |
Contexto unificado por dataset (cartão #146)
get_dataset_context / datasets://{dataset_id}/context chamam
GET /api/v1/datasets/{id}/context e devolvem, numa só leitura, o que antes
exigia 6–8 chamadas. Sem LLM; limitado a 6000 caracteres (JSON) — quando não
cabe, corta primeiro knowledge e depois schema e marca truncated: true com
truncated_fields. Cada secção traz available; sem dados (ex.: dataset sem
scan) vem available: false com reason em vez de erro.
| Secção | Conteúdo |
|---|---|
identity |
nome, schema/tabela, connector_type, camada medallion, domínio {id, name}, owners só por nome |
schema |
colunas: nome, tipo, nullable, descrição, sensibilidade e pii_type |
quality |
último score e dimensões, tendência dos últimos 5 scores, issues abertos por severidade/tipo, verdict, regras falhadas (sem valores observados) |
contract |
has_active, observed_only, versão ativa (number/status), conformidade (status + até 5 problemas) |
lineage |
upstream/downstream a 1 salto, contagem de nós externos, has_column_lineage, exposures só nome/tipo/maturidade |
readiness |
veredicto global de AI-readiness, por caso de uso e bloqueadores (sem amostras) |
trust |
trust score, grade, componentes com evidência, bloqueadores |
knowledge |
top-5 entradas da KB não obsoletas (corpo ≤ 300 chars) e excerto do README publicado (≤ 600 chars) |
usage |
classe de uso e queries_30d |
freshness |
last_modified_at do último snapshot de metadados e last_profiled_at |
Nenhum valor de amostra, email ou URL sai (texto livre é saneado — [email], [url]).
Os resources reutilizam o DataVaultClient e os summarize_* partilhados com
o chat — tools e resources devolvem exatamente os mesmos dados.
Readiness de data products
get_domain_readiness / domains://{domain_id}/readiness?use_case= fazem proxy
de GET /api/v1/domains/{id}/readiness e devolvem o JSON da API tal e qual (a
tool do chat devolve o mesmo, lido direto da sessão). Um agente ou pipeline que
pergunta «o data product Vendas está pronto para RAG?» faz uma chamada em vez de
N. Estados, códigos e o gate de CI: CI_QUALITY_GATE.md.
A readiness usa, por dataset, a mesma leitura da Trust API (#150) — as duas respostas nunca se contradizem — mas tem vocabulário próprio (agrega por produto):
Trust API (get_trust_decision, por dataset) |
Readiness (get_domain_readiness, por domínio) |
|---|---|
no_scan (decisão unknown) |
no_scan — bloqueador |
verdict_fail |
verdict_fail — bloqueador (e contract_invalid se o contrato ativo falhou) |
verdict_warn |
verdict_warn — aviso |
readiness_blocker |
ai_readiness_blocked — bloqueador |
readiness_low |
— (o score vem em datasets[].ai_readiness.score) |
scan_stale |
stale_scan — aviso (mesma regra: 2× a cadência) |
critical_incident (incidentes, com lineage) |
open_critical_issues — issues críticos do próprio dataset |
freshness_sla_breached |
slo_breach_open / attainment_below_objective — avisos, só com SLA no domínio |
pii_unmasked_gold, warning_incident |
— (usa a Trust API por dataset) |
| — (a Trust API é por dataset) | deprecated — bloqueador; draft — aviso: ciclo de vida do data product (cartão #168) |
Exemplo LangChain — verificar a readiness do produto antes de indexar (ao lado do gate por documento de RAG_GATE.md):
from beryx import BeryX
readiness = BeryX().domain_readiness(VENDAS_DOMAIN_ID, use_case="rag")
if not readiness.ready: # not_ready: não indexar
raise RuntimeError(readiness.summary())
vectorstore.add_documents(splitter.split_documents(loader.load()))
Instalação
cd backend
pip install -e ".[mcp]"
Configuração (Claude Desktop / Claude Code)
{
"mcpServers": {
"datavault": {
"command": "/caminho/backend/.venv/bin/python",
"args": ["-m", "app.mcp_server"],
"cwd": "/caminho/backend",
"env": {
"DATAVAULT_API_URL": "http://localhost:8000",
"DATAVAULT_EMAIL": "utilizador@empresa.pt",
"DATAVAULT_PASSWORD": "…"
}
}
}
}
Em Claude Code: claude mcp add datavault -- /caminho/backend/.venv/bin/python -m app.mcp_server
(com as mesmas variáveis de ambiente).
Modo Streamable HTTP (remoto)
Servidor de produção — https://mcp.praxis.lbostuff.tech/mcp
O MCP remoto está publicado (cartão #311): serviço Cloud Run datavault-mcp,
TLS gerido, sem nada a instalar. Basta um token de API dvk_ (Equipa → Tokens
de API, só admins) — a partir de qualquer máquina, sem este repositório:
claude mcp add --transport http datavault https://mcp.praxis.lbostuff.tech/mcp \
--header "Authorization: Bearer dvk_…"
Noutro cliente MCP com suporte HTTP (Cursor, VS Code, Codex, Claude Desktop
via mcp-remote), a página Ligar o teu agente da app gera a configuração
exata — ver AGENTES_CODIGO.md. Formato genérico:
{
"mcpServers": {
"datavault": {
"type": "http",
"url": "https://mcp.praxis.lbostuff.tech/mcp",
"headers": { "Authorization": "Bearer dvk_…" }
}
}
}
Testar com o MCP Inspector:
npx @modelcontextprotocol/inspector → transporte Streamable HTTP, URL
https://mcp.praxis.lbostuff.tech/mcp, header Authorization: Bearer dvk_….
Autenticação: cada pedido tem de trazer Authorization: Bearer dvk_….
Sem token, ou com um JWT de sessão, a resposta é 401. O token é validado
contra a própria API (revogação e workspace respeitados) e é reencaminhado em
todas as chamadas: cada cliente vê só o seu workspace. Se a API não responder,
503. OAuth 2.1 chega numa fatia seguinte (MCPV2).
Superfície só de leitura: em HTTP o servidor é stateless e não expõe
diagnose_issue — e os tokens dvk_* são read-only na API (qualquer método
que não GET dá 403), por isso nenhuma tool ou resource escreve.
Host e origem (anti-DNS-rebinding): a proteção do SDK só se liga sozinha
com bind em localhost. O servidor valida ele próprio o header Host,
antes da autenticação, contra uma allowlist: o host de
DATAVAULT_MCP_PUBLIC_URL mais os de DATAVAULT_MCP_ALLOWED_HOSTS (separados
por vírgula; em produção, o URL run.app determinístico). Fora da lista:
421. Um pedido com Origin (browser) só passa se a origem for https:// de
um host da lista (http:// só em loopback); senão 403. Com bind em
0.0.0.0 e a allowlist vazia, o servidor recusa arrancar. Com bind em
loopback, 127.0.0.1/localhost/::1 são aceites como no SDK.
Limites conhecidos:
- Revogação até 60 s: as validações positivas do token ficam em cache por instância durante 60 s (por hash — o token nunca é guardado nem logado). Um token revogado pode continuar a servir até lá.
- Rate limit por IP partilhado: a API aplica um limite por IP (1200
pedidos/min por instância,
DATAVAULT_API_RATE_LIMIT_IP_REQUESTS) e outro por token (300/min). Todos os clientes MCP chegam à API a partir dos IPs de saída dodatavault-mcp, por isso partilham o limite por IP; o limite por token continua a ser de cada cliente. O MCP não reencaminhaX-Forwarded-For(seria falsificável). Cada tool faz 1–3 pedidos à API. - Arranque a frio:
min-instances 0— o primeiro pedido depois de um período sem tráfego demora alguns segundos.
Operação (deploy)
deploy/gcp_deploy.sh (secção 5e) cria o serviço a partir da imagem do
backend (extra mcp no CONNECTOR_EXTRAS) com
python -m app.mcp_server --transport http --host 0.0.0.0 --port 8080:
| SA | datavault-mcp — só logging.logWriter e monitoring.metricWriter; sem Cloud SQL, sem segredos, sem VPC |
| Env | DATAVAULT_API_URL (https://api.${DOMAIN} — com o ingress fechado ao LB o run.app da api não responde a um Cloud Run sem VPC), DATAVAULT_MCP_PUBLIC_URL, DATAVAULT_MCP_ALLOWED_HOSTS |
| Escala | 1 vCPU · 512 MiB · concurrency 20 · min 0 · max MCP_MAX_INSTANCES (3) |
| Probe | arranque TCP 8080 (MCP_STARTUP_PROBE) — todo o /mcp exige Host e token |
| Domínio | Em produção, mcp.${DOMAIN} é um host do Application Load Balancer do edge (deploy/lb_apply.sh, DEPLOY_GCP.md §4); sem LB, map_domain datavault-mcp "mcp.${DOMAIN}" e CNAME mcp → ghs.googlehosted.com. (com DNS_ZONE/DNS_PROJECT o script cria-o) |
A api recebe DATAVAULT_MCP_PUBLIC_URL=https://mcp.${DOMAIN}/mcp, e é isso
que faz GET /ide/agent-config e a página «Ligar o teu agente» gerarem a
configuração real. O CD atualiza a imagem do datavault-mcp com a do backend.
O deploy/validate_prod.sh (passo 16c, deploy/check_mcp_remote.sh) prova de
fora, a cada deploy: o URL anunciado responde, sem token → 401, initialize
e tools/list com um token dvk_ descartável (criado, usado e revogado) têm
get_verdict e get_trust_decision, e um Host fora da allowlist → 421 (pelo
URL legado *.a.run.app do serviço, que chega ao contentor mas não está na
lista).
Servidor próprio (self-hosted / desenvolvimento)
Para correr o modo HTTP localmente (endpoint /mcp):
DATAVAULT_API_URL=http://localhost:8000 \
python -m app.mcp_server --transport http --host 127.0.0.1 --port 8765
Também configurável por ambiente: DATAVAULT_MCP_TRANSPORT=http,
DATAVAULT_MCP_HOST, DATAVAULT_MCP_PORT. Exposto noutra máquina (bind
0.0.0.0, atrás de TLS), define DATAVAULT_MCP_PUBLIC_URL com o URL público
— é a allowlist do Host. Na app, a mesma setting na api faz «Ligar o teu
agente» apontar para ele; vazia, as configs apontam para
http://127.0.0.1:8765/mcp e a página mostra o comando acima.
Recomendação (stdio, com
DATAVAULT_EMAIL/DATAVAULT_PASSWORD): cria um utilizadormemberdedicado ao MCP (POST /auth/users, só admins) — os members não podem apagar conexões nem convidar utilizadores.
Exemplos de perguntas ao agente
- «Qual é o dataset com pior score e quanto nos custa por mês?»
- «O dataset
clientespode ser publicado?» (usaget_verdict) - «Posso usar
gold.clientesnum agente? Quem é o dono e o que já correu mal?» (usaget_dataset_context) - «O meu agente pode ler
gold.clientesagora? E para RAG?» (usaget_trust_decision— a resposta diz também por quanto tempo vale) - «O que se sabe sobre a fiabilidade do
gold.encomendas? Quem é o owner e há PII sem máscara?» (usaget_trust_facts) - «Quantos clientes do CRM não existem no ERP?» (usa
match_datasets) - «Isto já aconteceu antes? Como resolvemos da última vez?» (usa
search_knowledge) - «Há issues critical abertos? Diagnostica o mais recente.»
- «Que sugestões de deduplicação temos com SQL pronto?»