@sbissoli/mcp-provenance
v0.2.0
Published
Contrato de proveniência para servidores MCP: todo retorno de tool carrega fonte, endpoint, período, data de extração e licença, em dois modos (concise/detailed), com serialização determinística
Maintainers
Readme
@sbissoli/mcp-provenance
Contrato de proveniência para servidores MCP: todo retorno de tool carrega fonte,
endpoint, período, data de extração e licença, com serialização determinística, em dois
modos — concise (padrão, piso legal) e detailed (bloco canônico completo).
Mantido para o meu portfólio de servidores MCP. Uso por terceiros é bem-vindo, mas o roadmap segue as necessidades dos meus servidores.
Especificação completa: docs/contrato-proveniencia-v1.md.
Uso
import { createProvenanceContext } from "@sbissoli/mcp-provenance";
// 1. Uma vez, na inicialização do servidor:
const prov = createProvenanceContext({
metaNamespace: "com.sidneybissoli.senado", // chaves de _meta (reverse-DNS, estável)
locale: "pt-BR", // idioma do rodapé ("pt-BR" | "en" | LocaleSpec)
timezone: { offset: "-03:00", label: "horário de Brasília" }, // default: "utc"
defaultMode: "concise", // default: "concise"
});
// 2. Presets por fonte upstream (opcional):
const SENADO_LEGIS = {
source: "Senado Federal — Dados Abertos (Legislativo)",
citation: "Fonte: Senado Federal, Portal de Dados Abertos (Legislativo) — legis.senado.leg.br/dadosabertos.",
license: "Dados Abertos do Senado Federal — uso livre com atribuição da fonte.",
};
// 3. Em cada tool:
const p = prov.from(SENADO_LEGIS, {
source_url: `${baseUrl}/processo.json`,
retrieved_at: fetchedAt, // instante REAL da extração (preservado pelo cache)
data_vintage: "2025",
});
return prov.result(shapedData, p); // modo concise (default)
return prov.result(shapedData, p, { mode: "detailed" }); // bloco canônico completoresult() emite os três canais: structuredContent (bloco + attribution RFC #711,
visível ao modelo), _meta namespaced (auditoria/UI, zero tokens) e rodapé de texto
compacto para clientes text-only.
Diagnóstico de origem (retrieval, contrato v1.1)
Como o dado foi obtido, para que o agente explique um dado instável em vez de inventar
certeza. É a 7ª chave do modo concise (depois de retrieved_at) e entra no bloco
canônico depois de served_from_cache. Só o que foi medido: servidor que não
instrumenta suas idas à origem omite o campo e ele sai null — nunca {attempts: 1}
inventado.
const p = prov.from(SENADO_LEGIS, {
source_url,
retrieved_at: fetchedAt,
retrieval: {
requests: 3, // idas distintas à origem nesta chamada (fatias, páginas)
attempts: 5, // tentativas somadas (>= requests)
anomalies: [{ kind: "timeout", count: 2 }], // opcional; classes: timeout | network | http_4xx |
}, // http_5xx | rate_limited | malformed_body
});
// → provenance.retrieval = { requests: 3, attempts: 5, anomalies: [{kind:"timeout",count:2}], unstable: true }unstable é derivado pela lib (attempts > requests ou alguma anomalia); anomalies é
somado por classe e ordenado no vocabulário, então a ordem de coleta não muda os bytes. No
rodapé, uma linha ao leitor aparece só quando instável: "Obtenção instável: 5
tentativas para 3 consultas à origem (2 tempos de resposta esgotados)." Semântica
completa em docs/contrato-proveniencia-v1.md §3;
regra de compatibilidade da linha 1.x em §8.
O outputSchema da tool: importe, não transcreva
O SDK do MCP valida structuredContent contra o outputSchema em runtime. Um servidor
que fecha o bloco de proveniência com as chaves transcritas à mão
(additionalProperties: false) e sobe o pacote sem reescrever a transcrição falha em
toda chamada — foi o que a medição de 26/09/2026 mostrou em quatro servidores. Desde a
0.2.0 o pacote publica a projeção que ele mesmo emite:
import { CONCISE_BLOCK_JSON_SCHEMA, ConciseBlockSchema } from "@sbissoli/mcp-provenance";
// outputSchema em JSON Schema verbatim (bcb, sih, medical):
const outputSchema = {
type: "object",
properties: { total: { type: "integer" }, provenance: CONCISE_BLOCK_JSON_SCHEMA, attribution: ATTRIBUTION },
required: ["total", "provenance", "attribution"],
};
// outputSchema em zod (ibge):
const outputSchema = z.object({ total: z.number().int(), provenance: ConciseBlockSchema, attribution: z.array(z.string()) });DETAILED_BLOCK_JSON_SCHEMA/DetailedBlockSchema e provenanceBlockJsonSchema(mode)
cobrem o modo detailed. Os testes do pacote prendem que schema e render* não divergem.
Fonte estruturada (ex.: ILOSTAT/SDMX)
const p = prov.build({
source: { name: "ILOSTAT", agency: "ILO", database: "ILOSTAT", endpoint: "https://sdmx.ilo.org/rest" },
dataset: { id: "DF_UNE_DEAP_SEX_AGE_RT", version: "1.0", name: "Unemployment rate by sex and age" },
dimension_key: { REF_AREA: "BRA", SEX: "SEX_F", TIME_PERIOD: "2024" },
data_vintage: "2026-06-15",
retrieved_at: fetchedAt,
source_url: canonicalRestUrl,
license: { id: "CC-BY-4.0", url: "https://creativecommons.org/licenses/by/4.0/", verified_at: "2026-08-04" },
citation: "International Labour Organization, ILOSTAT, https://ilostat.ilo.org/data/, accessed 2026-08-04.",
});Multi-fonte (segregação de licenças)
// Um bloco POR FONTE; dados de cada fonte em estruturas separadas apontando para o seu bloco.
return prov.result({ ilostat: dadosIlo, uis: dadosUis }, [pIlo, pUis]);Recortes múltiplos de uma mesma fonte
const p = prov.build({ ...base, field_sources: [
{ fields: ["relatoria"], source_url: urlRelatoria, retrieved_at: fetchedAtRelatoria },
]});Regras que a lib impõe (server-side, antes de responder)
licensecom ao menosidouname(piso legal);derived: trueexigederivation_note;- chaves em ordem fixa e ausência como
nullexplícito (determinismo byte-a-byte por modo); - timestamps ISO-8601 sem milissegundos, normalizados ao fuso configurado (datas puras passam intactas — nunca inventa horário num vintage).
API
createProvenanceContext(options) → { build, from, render, footer, result, metaKeys }.
Peças soltas também exportadas: CanonicalProvenanceSchema, renderConcise,
renderDetailed, attributionList, provenanceFooter, toCanonicalIso, locales
ptBR/en.
