Descoberta de agentes
Como os agentes encontram sua superfície legível por máquina sem adivinhar — o manifesto de legibilidade para agentes, cabeçalhos Link, o catálogo de APIs da RFC 9727, WebMCP, a skill gerada do seu site e as skills publicadas, descoberta via DNS e chaves de Web Bot Auth.
Publicar llms.txt, espelhos em Markdown, uma API JSON e um servidor MCP é só metade do trabalho — um agente ainda precisa encontrar tudo isso. O Blume anuncia a superfície inteira pelas convenções que os agentes de fato sondam: um manifesto na raiz do site, cabeçalhos Link e tags <link>, arquivos well-known e o próprio model context do navegador. Tudo aqui vem ligado por padrão e é derivado do que você já habilitou.
Legibilidade para agentes
O Blume escreve um manifesto /agent-readability.json na raiz do seu site que indexa a superfície voltada a agentes descrita ao longo desta seção — assim um agente consegue descobri-la com uma única requisição, em vez de adivinhar convenções ou raspar HTML. Como o llms.txt, ele vem ligado por padrão:
agents: {
agentReadability: true,
}
O manifesto lista apenas o que você habilitou — o padrão de espelho em Markdown bruto, a API JSON e sua descrição OpenAPI, llms.txt e llms-full.txt, o servidor MCP e seu documento de descoberta, o endpoint do assistente, o sitemap e os feeds RSS — junto com o nome do seu site, a descrição, o repositório de origem e a política de uso do content-signal. As URLs são absolutas quando deployment.site está definido, e relativas à raiz caso contrário:
{
"artifacts": {
"markdown": {
"contentNegotiation": "text/markdown",
"pattern": "https://docs.example.com/{route}.md"
},
"api": {
"openapi": "https://docs.example.com/openapi.json",
"pages": "https://docs.example.com/api/docs/pages.json",
"search": "https://docs.example.com/api/docs/search"
},
"llmsFullTxt": "https://docs.example.com/llms-full.txt",
"llmsTxt": "https://docs.example.com/llms.txt",
"mcp": {
"discovery": "https://docs.example.com/.well-known/mcp.json",
"url": "https://docs.example.com/mcp"
}
},
"description": "Docs for the Acme API.",
"generator": "blume@2.0.0",
"name": "Acme Docs",
"site": "https://docs.example.com",
"contentUsage": { "search": true, "ai-input": true, "ai-train": true },
"repository": "https://github.com/acme/docs"
}
O campo contentNegotiation aparece apenas quando o site publicado realmente respeita o cabeçalho Accept: text/markdown — veja negociação de conteúdo; em todo outro tipo de deploy, o manifesto anuncia somente o padrão de espelho .md.
Defina agents.agentReadability como false para pular isso, ou publique seu próprio public/agent-readability.json para assumir o controle — o Blume nunca sobrescreve um arquivo que você coloca em public/.
Cabeçalho Link de descoberta
Agentes que sondam um site não sabem que devem procurar pelo manifesto — então o Blume também o anuncia em um cabeçalho de resposta Link da RFC 8288 na página inicial, usando tipos de relação registrados na IANA:
Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json; profile=\"https://www.rfc-editor.org/info/rfc9727\"",
</.well-known/ai-catalog.json>; rel="ai-catalog"; type="application/ai-catalog+json",
</openapi.json>; rel="service-desc"; type="application/json",
</agent-readability.json>; rel="describedby"; type="application/json",
</llms.txt>; rel="describedby"; type="text/plain",
</index.md>; rel="alternate"; type="text/markdown"
Cada entrada aparece somente quando o recurso correspondente está ativo. O link alternate aponta para o espelho em Markdown da página inicial — o Markdown bruto da própria página quando a rota inicial é uma página de conteúdo, ou o fallback sintetizado do llms.txt quando é uma landing page. O link service-desc (RFC 8631) aponta para a descrição OpenAPI da API JSON, o api-catalog para o catálogo de APIs gerado, e o ai-catalog para o catálogo de IA. O cabeçalho acompanha todas as superfícies que o Blume controla: builds estáticos via o arquivo _headers emitido (Netlify e Cloudflare), builds de servidor na Vercel via as regras de roteamento do deploy e o servidor de desenvolvimento (confira com curl -I localhost:4321). O cabeçalho do servidor de desenvolvimento lista apenas o que ele serve — os links service-desc e alternate —, porque é o blume build que escreve os catálogos, o agent-readability.json e o llms.txt na saída do build.
Nem todo agente entra pela raiz, no entanto — um que siga um resultado de busca ou um link compartilhado cai numa página profunda e nunca vê o cabeçalho da página inicial. Por isso, toda página renderizada também carrega os mesmos links de descoberta no seu <head> HTML, usando as mesmas relações registradas na IANA:
<link
rel="describedby"
href="/agent-readability.json"
type="application/json"
/>
<link rel="describedby" href="/llms.txt" type="text/plain" />
<link
rel="ai-catalog"
href="/.well-known/ai-catalog.json"
type="application/ai-catalog+json"
/>
<link rel="ard" href="/.well-known/ard.json" type="application/json" />
<link rel="alternate" href="/docs/example.md" type="text/markdown" />
Aqui o link alternate aponta para o espelho em Markdown bruto daquela página em si, para que o agente possa pular direto do HTML em que caiu para a versão eficiente em tokens. Como os links do head viajam junto com o HTML pré-renderizado, eles também funcionam em hosts que ignoram _headers e não conseguem enviar cabeçalhos de resposta personalizados (GitHub Pages, S3) — não importa em qual página o agente entre.
Catálogo de APIs
Quando o site publica APIs, o Blume gera um catálogo de APIs da RFC 9727 em /.well-known/api-catalog — um linkset que permite aos agentes enumerar suas APIs a partir apenas do domínio, servido em todas as superfícies de build com seu media type registrado application/linkset+json e o parâmetro profile="https://www.rfc-editor.org/info/rfc9727" da RFC. Não há nada para configurar: o catálogo é derivado do que já está no blume.config.ts. Cada referência OpenAPI, AsyncAPI ou GraphQL vira uma entrada ancorada na sua rota de docs renderizada, com service-doc apontando para essa documentação e service-desc para a spec quando ela está em uma URL acessível; a própria API JSON do site vira uma entrada descrita por seu /openapi.json; e o servidor MCP vira uma entrada com seu documento de descoberta como descrição do serviço:
{
"linkset": [
{
"anchor": "https://docs.example.com/reference",
"service-doc": [
{ "href": "https://docs.example.com/reference", "type": "text/html" }
],
"service-desc": [{ "href": "https://api.example.com/openapi.json" }]
},
{
"anchor": "https://docs.example.com/api/docs",
"service-desc": [
{
"href": "https://docs.example.com/openapi.json",
"type": "application/json"
}
],
"service-doc": [
{ "href": "https://docs.example.com/", "type": "text/html" }
]
},
{
"anchor": "https://docs.example.com/mcp",
"service-desc": [
{
"href": "https://docs.example.com/.well-known/mcp.json",
"type": "application/json"
}
],
"service-doc": [
{ "href": "https://docs.example.com/", "type": "text/html" }
]
}
]
}
Um site sem referências de API, sem servidor MCP e com a API JSON desligada não emite catálogo algum — não haveria nada nele. Como em todo lugar, um arquivo public/.well-known/api-catalog que você mesmo publique vence o gerado.
Catálogo de IA
O catálogo de APIs lista APIs. O catálogo de IA lista tudo o que um agente poderia pegar do site — o servidor MCP, cada skill publicada, a API JSON, cada referência de API renderizada e o llms.txt — no formato que os registries de agentes indexam: um documento AI Catalog em /.well-known/ai-catalog.json, que também é o manifesto que os consumidores do Agentic Resource Discovery (ARD) resolvem. Cada entrada traz um identificador urn:air:<host>:<namespace>:<name> ancorado no domínio, um nome de exibição, uma descrição de uma linha, o media type do artefato, sua URL e um punhado de representativeQueries — perguntas de exemplo que o recurso consegue responder, que os registries convertem em embeddings para busca semântica. Este é o catálogo de um site intitulado “Acme” com o servidor MCP ligado e uma skill publicada:
{
"specVersion": "1.0",
"host": {
"displayName": "Acme",
"identifier": "did:web:docs.example.com",
"documentationUrl": "https://docs.example.com/"
},
"entries": [
{
"identifier": "urn:air:docs.example.com:mcp:acme",
"displayName": "Acme",
"description": "Model Context Protocol server over the Acme documentation: full-text search, page Markdown, the page index, and the navigation tree.",
"type": "application/mcp-server-card+json",
"url": "https://docs.example.com/.well-known/mcp/server-card.json",
"capabilities": [
"search_docs",
"get_page",
"list_pages",
"get_navigation"
],
"representativeQueries": [
"search the Acme documentation",
"get a page of the Acme docs as Markdown",
"list every page in the Acme docs"
]
},
{
"identifier": "urn:air:docs.example.com:skill:acme",
"displayName": "acme",
"description": "Set up an Acme project and call its API.",
"type": "application/agent-skills+md",
"url": "https://docs.example.com/.well-known/agent-skills/acme/SKILL.md",
"representativeQueries": [
"load the acme agent skill",
"how do I use acme"
]
},
{
"identifier": "urn:air:docs.example.com:api:docs",
"displayName": "Acme docs API",
"description": "REST API over the Acme documentation: the page index, each page as JSON or Markdown, and the navigation tree, described by this OpenAPI document.",
"type": "application/vnd.oai.openapi+json",
"url": "https://docs.example.com/openapi.json",
"representativeQueries": [
"fetch a page of the Acme docs as JSON",
"list the pages in the Acme docs",
"get the navigation tree of the Acme docs"
]
},
{
"identifier": "urn:air:docs.example.com:docs:llms-txt",
"displayName": "Acme llms.txt",
"description": "llms.txt index of the Acme documentation: every page with a one-line summary, plus the agent-facing resources on this site.",
"type": "text/plain",
"url": "https://docs.example.com/llms.txt",
"representativeQueries": [
"what is Acme",
"overview of the Acme documentation"
]
}
]
}
A revisão atual do ARD lê o manifesto em /.well-known/ard.json e chama o ai-catalog.json de caminho predecessor, então o Blume escreve o mesmo documento nos dois lugares, o anuncia sob ambas as relações de link (ai-catalog e ard) no head de toda página, e o lista no llms.txt e no agent-readability.json. O catálogo e seus vizinhos em .well-known (o catálogo de APIs, os arquivos de descoberta do MCP) são servidos com Access-Control-Allow-Origin: * em todas as superfícies de build, então um registry que os leia de outra origem não é bloqueado.
Os identificadores das entradas são ancorados no seu domínio, então o catálogo precisa de um deployment.site — sem ele nada é emitido. Vem ligado por padrão; agents.catalog: false desativa. As queries geradas são derivadas do título do site e do próprio nome de cada entrada. Para escrever as suas próprias para uma entrada, indexe-as pela cauda do identificador (<namespace>:<name>):
export default defineConfig({
agents: {
catalog: {
queries: {
"mcp:acme": ["how do I install Acme", "search the Acme docs"],
"skill:acme": ["set up an Acme project", "write an Acme plugin"],
},
},
},
});
Uma lista substitui as queries geradas daquela entrada; as entradas que você não nomear mantêm as suas. Como em todo lugar, um public/.well-known/ai-catalog.json (ou ard.json) que você mesmo publique vence o gerado.
WebMCP
O WebMCP é uma API de navegador emergente que permite a uma página registrar ferramentas diretamente com um navegador agêntico — sem necessidade de conexão a um servidor separado. Toda página do Blume registra a superfície somente-leitura da documentação no model context da página: search_docs (busca no site), get_page (o Markdown bruto de uma página) e list_pages (o índice do llms.txt). O script é minúsculo, não carrega nenhuma maquinaria de busca até que uma ferramenta seja de fato chamada, e vira um no-op silencioso em todo navegador sem a API — o que hoje significa todos, fora o preview inicial do Chrome. Ele se registra em qualquer superfície que a spec (ainda em fluxo) exponha (navigator.modelContext ou document.modelContext), via provideContext ou registerTool por ferramenta.
Vem ligado por padrão; defina webmcp: false para desativar:
agents: {
webmcp: false,
}
A skill do seu site
Todo build escreve uma agent skill para a sua documentação, um SKILL.md que um agente de código pode instalar para trabalhar com o seu produto. Ela é servida em /skill.md e listada no índice de skills. O Blume a monta a partir do que já sabe sobre o site, sem chamar um modelo, então ela não custa nada e muda sempre que a sua documentação muda. Ela contém:
- o título e a descrição do site, e quando um agente deve usar a skill
- como ler qualquer página como Markdown, além do
llms.txt, dollms-full.txt, do servidor MCP, da API JSON e do changelog, quando o site os serve - suas referências de API
- um mapa da documentação na ordem da barra lateral, com um link para o Markdown de cada página junto da sua descrição de uma linha (a meta description da página, como no
llms.txt)
A skill leva o nome do title do seu site (Acme Docs vira acme-docs). Ela cobre a versão atual no idioma padrão, deixa de fora as páginas de operações de API e as entradas do changelog (cada um desses tem uma linha própria acima do mapa), pula as mesmas páginas que o llms.txt pula (rascunhos, páginas ocultas e noindex, e páginas com ai.exclude) e lista até 200 páginas, apontando para o llms.txt para o restante. Os links são absolutos, então a skill precisa de deployment.site; um build sem ele a pula.
A skill gerada é um guia da sua documentação, não um resumo do seu produto. Para ter uma que ensine o seu produto a um agente — configuração, conceitos centrais, tarefas comuns e armadilhas —, peça ao seu agente de código que a escreva a partir da documentação:
blume skill --claude # or --codex
O blume skill abre o Claude Code ou o Codex na skill blume-write-skill, que escreve um SKILL.md dentro da sua pasta agents.skills (./skills quando ela não está definida, adicionando-a à sua configuração), com o nome do seu site para que ela substitua a gerada. Você revisa e faz commit dela como de qualquer outro arquivo; ela gasta tokens do seu agente uma única vez, não a cada build. Rode de novo depois de uma grande mudança na documentação, já que uma skill escrita à mão não se atualiza sozinha. Sem flag, o blume skill mostra onde a skill ficaria e como apontar outro agente para ela.
Qualquer skill com o mesmo nome em agents.skills, não importa como você a escreva, substitui a gerada no índice, e também em /skill.md quando é um SKILL.md solitário. Um public/skill.md que você publique vence as duas. Para desligar a skill:
agents: {
skillMd: false,
}
Descoberta de skills
Se o seu projeto publica agent skills — o próprio repositório do Blume publica —, aponte agents.skills para o diretório que as contém, e o build as publica para descoberta conforme a RFC de Agent Skills Discovery:
agents: {
skills: "./skills",
}
O caminho é resolvido a partir da raiz do seu projeto, e cada subdiretório com um SKILL.md vira uma skill publicada. Uma skill que é apenas um SKILL.md solitário é copiada literalmente para /.well-known/agent-skills/<name>/SKILL.md (type: "skill-md"); uma skill com recursos de apoio (scripts/, references/, assets/) é empacotada em um .tar.gz determinístico (type: "archive") para que suas referências relativas resolvam após a extração, com os bits de execução dos scripts preservados. O índice de descoberta em /.well-known/agent-skills/index.json traz o $schema v0.2.0 e, por skill, seu nome, tipo, descrição (do frontmatter do SKILL.md), URL do artefato e o digest SHA-256 que os clientes usam para verificar os downloads.
Skills com name/description ausentes ou inválidos segundo a spec são ignoradas com um aviso de build, em vez de publicadas quebradas, e um public/.well-known/agent-skills/index.json que você mesmo publique assume o controle de toda a superfície. As skills publicadas, incluindo a do seu próprio site, também são listadas no llms.txt.
Descoberta via DNS (DNS-AID)
O DNS for AI Discovery é um draft emergente da IETF que permite aos agentes descobrir a superfície de IA de um site antes de fazer uma única requisição HTTP, consultando registros SVCB/HTTPS em ServiceMode em um ponto de entrada DNS well-known. Registros DNS vivem na sua zona, não no build, então essa é a única superfície de descoberta que o Blume não pode publicar por você — em vez disso, adicione um registro no seu provedor de DNS:
_index._agents.docs.example.com. 3600 IN HTTPS 1 docs.example.com. alpn=h2
Use o tipo de registro HTTPS se o seu provedor oferecer (o Vercel DNS oferece; ele não suporta o tipo SVCB puro), ou um registro SVCB em ServiceMode com os parâmetros alpn e port caso contrário. O draft também recomenda assinar a zona com DNSSEC para que resolvedores validadores retornem respostas autenticadas — provedores como a Cloudflare habilitam isso com um clique, enquanto alguns (incluindo o Vercel DNS) simplesmente não suportam.
O blume audit --url <origin> verifica isso por você: quando deployment.site está definido, a camada de rede consulta o ponto de entrada via DNS-over-HTTPS e informa o registro exato a publicar caso não exista nenhum, além de dizer se as respostas são autenticadas por DNSSEC. Defina BLUME_DOH_URL para apontar a consulta ao seu próprio resolvedor se a sua rede bloquear os públicos (Google, Cloudflare).
Web Bot Auth
O Web Bot Auth funciona na direção oposta: não se trata de agentes lendo a sua documentação, mas de os agentes da sua organização se identificarem ao fazer requisições em outros lugares. Seus agentes assinam as requisições com HTTP Message Signatures, e os sites receptores as verificam contra um diretório de chaves públicas publicado no seu domínio. Se a sua organização opera agentes e o seu site Blume vive no domínio com que eles se identificam, publique as chaves públicas deles:
agents: {
webBotAuth: {
keys: [{ kty: "OKP", crv: "Ed25519", x: "JrQLj5P_89iXES9-vFgrIy29c…" }],
},
}
O Blume então serve o JWKS em /.well-known/http-message-signatures-directory com seu media type registrado em todas as superfícies de build. O diretório é público por definição, então a configuração só admite chaves públicas — um JWK contendo material privado (d, p, q, …) falha na validação com um erro em vez de publicar uma credencial vazada. Gere um par Ed25519 com:
node -e 'const { generateKeyPairSync } = require("node:crypto"); const { publicKey, privateKey } = generateKeyPairSync("ed25519"); console.log("public: ", JSON.stringify(publicKey.export({ format: "jwk" }))); console.log("private:", JSON.stringify(privateKey.export({ format: "jwk" })))'
O JWK público vai na configuração acima; o privado vai onde quer que o seu agente de assinatura rode (um gerenciador de segredos, nunca o repositório). Se a sua organização não opera agentes, pule isso — um diretório vazio não anuncia nada que valha a pena verificar.
Como o blume.config.ts é executado em tempo de build, a chave não precisa ficar hardcoded — carregue-a de uma variável de ambiente de build para manter a configuração livre de blobs de chave e rotacionar sem um commit:
const webBotAuthKey = process.env.WEB_BOT_AUTH_PUBLIC_JWK;
export default defineConfig({
agents: {
webBotAuth: {
keys: webBotAuthKey ? [JSON.parse(webBotAuthKey)] : [],
},
},
});
Ambientes sem a variável não publicam diretório algum, e uma chave carregada dessa forma é validada exatamente como uma inline — incluindo a checagem de material privado. (A chave pública não é um segredo, então commitá-la inline é igualmente válido; a variável de ambiente é uma escolha de ergonomia, não de segurança.)