Agent-Discovery
Wie Agenten deine maschinenlesbare Oberfläche finden, ohne zu raten — das Agent-Readability-Manifest, Link-Header, der RFC-9727-API-Katalog, WebMCP, der generierte Skill deiner Site und veröffentlichte Skills, DNS-basierte Discovery und Web-Bot-Auth-Schlüssel.
llms.txt, Markdown-Spiegel, eine JSON-API und einen MCP-Server zu veröffentlichen, ist nur die halbe Arbeit — ein Agent muss sie auch finden. Blume bewirbt die gesamte Oberfläche über die Konventionen, die Agenten tatsächlich abfragen: ein Manifest im Site-Root, Link-Header und <link>-Tags, Well-Known-Dateien und den Model Context des Browsers selbst. Alles hier ist standardmäßig aktiv und leitet sich aus dem ab, was du bereits aktiviert hast.
Agent Readability
Blume schreibt ein /agent-readability.json-Manifest in dein Site-Root, das die in diesem Abschnitt beschriebene agentenseitige Oberfläche indexiert — so kann ein Agent sie mit einem einzigen Fetch entdecken, statt Konventionen zu erraten oder HTML zu scrapen. Wie llms.txt ist es standardmäßig aktiv:
agents: {
agentReadability: true,
}
Das Manifest listet nur, was du aktiviert hast — das Spiegelmuster für rohes Markdown, die JSON-API und ihre OpenAPI-Beschreibung, llms.txt und llms-full.txt, den MCP-Server und sein Discovery-Dokument, den Endpunkt des Assistenten, die Sitemap und RSS-Feeds — zusammen mit deinem Site-Namen, der Beschreibung, dem Quell-Repository und der Content-Signal-Nutzungsrichtlinie. URLs sind absolut, wenn deployment.site gesetzt ist, und ansonsten root-relativ:
{
"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"
}
Das Feld contentNegotiation erscheint nur, wenn die deployte Site den Header Accept: text/markdown tatsächlich berücksichtigt — siehe Content Negotiation; auf jedem anderen Deployment bewirbt das Manifest lediglich das .md-Spiegelmuster.
Setze agents.agentReadability auf false, um es zu überspringen, oder liefere deine eigene public/agent-readability.json aus, um die Kontrolle zu übernehmen — Blume überschreibt niemals eine Datei, die du in public/ ablegst.
Discovery-Link-Header
Agenten, die eine Site abklopfen, wissen nicht, dass sie nach dem Manifest suchen sollen — deshalb bewirbt Blume es zusätzlich in einem RFC 8288-Link-Response-Header auf der Startseite, unter Verwendung IANA-registrierter Relationstypen:
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"
Jeder Eintrag erscheint nur, wenn sein Feature aktiv ist. Der alternate-Link zeigt auf den Markdown-Spiegel der Startseite — das rohe Markdown der Seite selbst, wenn die Home-Route eine Content-Seite ist, oder den synthetisierten llms.txt-Fallback, wenn es eine Landingpage ist. Der service-desc-Link (RFC 8631) zeigt auf die OpenAPI-Beschreibung der JSON-API, api-catalog auf den generierten API-Katalog und ai-catalog auf den KI-Katalog. Der Header wird auf jeder Oberfläche mitgeliefert, die Blume kontrolliert: statischen Builds über die erzeugte _headers-Datei (Netlify und Cloudflare), Vercel-Server-Builds über die Routing-Regeln des Deployments und dem Dev-Server (prüfe ihn mit curl -I localhost:4321). Der Header des Dev-Servers listet nur, was er selbst ausliefert — die service-desc- und alternate-Links —, da blume build die Kataloge, agent-readability.json und llms.txt in die Build-Ausgabe schreibt.
Nicht jeder Agent betritt die Site jedoch über das Root — wer einem Suchergebnis oder einem geteilten Link folgt, landet auf einer tiefen Seite und sieht den Startseiten-Header nie. Deshalb trägt auch jede gerenderte Seite dieselben Discovery-Links in ihrem HTML-<head>, mit denselben IANA-registrierten Relationen:
<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" />
Hier zeigt der alternate-Link auf den seiteneigenen Roh-Markdown-Spiegel, sodass ein Agent direkt von dem HTML, auf dem er gelandet ist, zur token-effizienten Version springen kann. Da die Head-Links mit dem vorgerenderten HTML mitreisen, funktionieren sie auch auf Hosts, die _headers ignorieren und überhaupt keine eigenen Response-Header senden können (GitHub Pages, S3) — ganz gleich, auf welcher Seite der Agent einsteigt.
API-Katalog
Wenn die Site APIs veröffentlicht, generiert Blume unter /.well-known/api-catalog einen RFC 9727-API-Katalog — ein Linkset, mit dem Agenten deine APIs allein anhand der Domain aufzählen können, ausgeliefert auf jeder Build-Oberfläche mit seinem registrierten Medientyp application/linkset+json und dem profile="https://www.rfc-editor.org/info/rfc9727"-Parameter des RFC. Es gibt nichts zu konfigurieren: Der Katalog wird aus dem abgeleitet, was ohnehin in blume.config.ts steht. Jede OpenAPI-, AsyncAPI- oder GraphQL-Referenz wird zu einem Eintrag, verankert an ihrer gerenderten Docs-Route, wobei service-doc auf diese Docs zeigt und service-desc auf die Spezifikation, sofern sie unter einer abrufbaren URL liegt; die JSON-API der Site selbst wird zu einem Eintrag, der durch ihre /openapi.json beschrieben wird; und der MCP-Server wird zu einem Eintrag mit seinem Discovery-Dokument als Service-Beschreibung:
{
"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" }
]
}
]
}
Eine Site ohne API-Referenzen, ohne MCP-Server und mit abgeschalteter JSON-API gibt keinen Katalog aus — es stünde nichts darin. Wie überall gewinnt eine public/.well-known/api-catalog-Datei, die du selbst auslieferst, über die generierte.
KI-Katalog
Der API-Katalog listet APIs. Der KI-Katalog listet alles, was ein Agent von der Site aufgreifen könnte — den MCP-Server, jeden veröffentlichten Skill, die JSON-API, jede gerenderte API-Referenz und llms.txt — in dem Format, das Agenten-Registries indexieren: ein AI Catalog-Dokument unter /.well-known/ai-catalog.json, das zugleich das Manifest ist, das Agentic-Resource-Discovery-(ARD)-Clients auflösen. Jeder Eintrag trägt einen domain-verankerten urn:air:<host>:<namespace>:<name>-Identifier, einen Anzeigenamen, eine einzeilige Beschreibung, den Medientyp des Artefakts, dessen URL und eine Handvoll representativeQueries — Beispielfragen, die die Ressource beantworten kann und die Registries für die semantische Suche einbetten. Hier ist der Katalog für eine Site mit dem Titel „Acme“, aktiviertem MCP-Server und einem veröffentlichten Skill:
{
"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"
]
}
]
}
Die aktuelle ARD-Revision liest das Manifest von /.well-known/ard.json und bezeichnet ai-catalog.json als Vorgängerpfad, deshalb schreibt Blume dasselbe Dokument an beide Orte, bewirbt es im Head jeder Seite unter beiden Link-Relationen (ai-catalog und ard) und führt es in llms.txt und agent-readability.json auf. Der Katalog und seine .well-known-Nachbarn (der API-Katalog, die MCP-Discovery-Dateien) werden auf jeder Build-Oberfläche mit Access-Control-Allow-Origin: * ausgeliefert, sodass eine Registry, die sie von einem anderen Origin liest, nicht blockiert wird.
Eintrags-Identifier sind auf deiner Domain verankert, daher braucht der Katalog eine deployment.site — ohne sie wird nichts ausgegeben. Er ist standardmäßig aktiv; agents.catalog: false schaltet ihn ab. Die generierten Queries leiten sich aus dem Site-Titel und dem jeweils eigenen Namen des Eintrags ab. Um eigene für einen Eintrag zu schreiben, schlüssele sie nach dem Ende des Identifiers (<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"],
},
},
},
});
Eine Liste ersetzt die generierten Queries für diesen Eintrag; Einträge, die du nicht benennst, behalten ihre. Wie überall gewinnt eine public/.well-known/ai-catalog.json (oder ard.json), die du selbst auslieferst, über die generierte.
WebMCP
WebMCP ist eine aufkommende Browser-API, mit der eine Seite Tools direkt bei einem agentischen Browser registrieren kann — ohne separate Serververbindung. Jede Blume-Seite registriert die schreibgeschützte Oberfläche der Docs im Model Context der Seite: search_docs (Site-Suche), get_page (das rohe Markdown einer Seite) und list_pages (der llms.txt-Index). Das Skript ist winzig, lädt keinerlei Suchmaschinerie, bevor tatsächlich ein Tool aufgerufen wird, und bleibt in jedem Browser ohne diese API still wirkungslos — was heute auf alle außerhalb von Chromes Early Preview zutrifft. Es registriert sich auf derjenigen Oberfläche, die die noch im Fluss befindliche Spezifikation bereitstellt (navigator.modelContext oder document.modelContext), per provideContext oder tool-weisem registerTool.
Es ist standardmäßig aktiv; setze webmcp: false, um es abzuschalten:
agents: {
webmcp: false,
}
Der Skill deiner Site
Jeder Build schreibt einen Agent Skill für deine Docs — eine SKILL.md, die ein Coding-Agent installieren kann, um mit deinem Produkt zu arbeiten. Er wird unter /skill.md ausgeliefert und im Skills-Index aufgeführt. Blume erstellt ihn aus dem, was es ohnehin über die Site weiß, ohne ein Modell aufzurufen — er kostet also nichts und ändert sich, sobald sich deine Docs ändern. Er enthält:
- den Titel und die Beschreibung der Site sowie wann ein Agent den Skill verwenden sollte
- wie man jede Seite als Markdown liest, dazu
llms.txt,llms-full.txt, den MCP-Server, die JSON-API und das Changelog, sofern die Site sie ausliefert - deine API-Referenzen
- eine Übersicht der Docs in Sidebar-Reihenfolge, die das Markdown jeder Seite mit ihrer einzeiligen Beschreibung verlinkt (ihrer Meta-Beschreibung, wie in
llms.txt)
Der Skill ist nach dem title deiner Site benannt (aus Acme Docs wird acme-docs). Er deckt die aktuelle Version in der Standardsprache ab, lässt API-Operationsseiten und Changelog-Einträge aus (beide haben jeweils eine eigene Zeile oberhalb der Übersicht), überspringt dieselben Seiten wie llms.txt (Entwürfe, versteckte und noindex-Seiten sowie Seiten mit ai.exclude) und listet bis zu 200 Seiten auf, wobei er für den Rest auf llms.txt verweist. Die Links sind absolut, daher braucht der Skill deployment.site; ein Build ohne sie überspringt ihn.
Der generierte Skill ist ein Wegweiser durch deine Docs, keine Zusammenfassung deines Produkts. Für einen, der einem Agenten dein Produkt beibringt — Setup, Kernkonzepte, häufige Aufgaben und Stolperfallen —, lass deinen Coding-Agenten ihn aus den Docs schreiben:
blume skill --claude # or --codex
blume skill öffnet Claude Code oder Codex mit dem Skill blume-write-skill, der eine SKILL.md in deinem agents.skills-Ordner schreibt (./skills, wenn keiner gesetzt ist — dann wird er deiner Konfiguration hinzugefügt), benannt nach deiner Site, sodass er den generierten ersetzt. Du prüfst und committest ihn wie jede andere Datei; das kostet deinen Agenten einmalig Tokens, nicht bei jedem Build. Führe blume skill nach einer größeren Änderung an den Docs erneut aus, denn ein handgeschriebener Skill aktualisiert sich nicht von selbst. Ohne Flag gibt blume skill aus, wo der Skill landen würde und wie du einen anderen Agenten darauf ansetzt.
Jeder gleichnamige Skill in agents.skills, egal wie du ihn schreibst, ersetzt den generierten im Index — und auch unter /skill.md, wenn er nur aus einer einzelnen SKILL.md besteht. Eine public/skill.md, die du selbst auslieferst, gewinnt über beide. Um den Skill abzuschalten:
agents: {
skillMd: false,
}
Skills-Discovery
Wenn dein Projekt Agent Skills ausliefert — das Blume-Repo selbst tut das —, richte agents.skills auf das Verzeichnis, das sie enthält, und der Build veröffentlicht sie gemäß dem Agent Skills Discovery RFC zur Discovery:
agents: {
skills: "./skills",
}
Der Pfad wird relativ zu deinem Projekt-Root aufgelöst, und jedes Unterverzeichnis mit einer SKILL.md wird zu einem veröffentlichten Skill. Ein Skill, der nur aus einer einzelnen SKILL.md besteht, wird wortgetreu nach /.well-known/agent-skills/<name>/SKILL.md kopiert (type: "skill-md"); ein Skill mit unterstützenden Ressourcen (scripts/, references/, assets/) wird in ein deterministisches .tar.gz gebündelt (type: "archive"), damit sich seine relativen Verweise nach dem Entpacken auflösen, wobei die Ausführungsbits von Skripten erhalten bleiben. Der Discovery-Index unter /.well-known/agent-skills/index.json trägt das $schema der Version v0.2.0 und pro Skill dessen Namen, Typ, Beschreibung (aus dem Frontmatter der SKILL.md), Artefakt-URL und den SHA-256-Digest, gegen den Clients Downloads verifizieren.
Skills mit fehlendem oder spezifikationswidrigem name/description werden mit einer Build-Warnung übersprungen, statt kaputt veröffentlicht zu werden, und eine public/.well-known/agent-skills/index.json, die du selbst auslieferst, übernimmt die gesamte Oberfläche. Veröffentlichte Skills, einschließlich des eigenen Skills deiner Site, werden außerdem in llms.txt aufgeführt.
DNS-basierte Discovery (DNS-AID)
DNS for AI Discovery ist ein aufkommender IETF-Draft, mit dem Agenten die KI-Oberfläche einer Site entdecken können, bevor sie auch nur einen einzigen HTTP-Request stellen — indem sie ServiceMode-SVCB/HTTPS-Records an einem Well-Known-DNS-Einstiegspunkt abfragen. DNS-Records liegen in deiner Zone, nicht im Build, daher ist dies die eine Discovery-Oberfläche, die Blume nicht für dich veröffentlichen kann — stattdessen legst du bei deinem DNS-Anbieter einen Record an:
_index._agents.docs.example.com. 3600 IN HTTPS 1 docs.example.com. alpn=h2
Verwende den Record-Typ HTTPS, wenn dein Anbieter ihn anbietet (Vercel DNS tut das; den reinen SVCB-Typ unterstützt es nicht), andernfalls einen ServiceMode-SVCB-Record mit den Parametern alpn und port. Der Draft empfiehlt außerdem, die Zone mit DNSSEC zu signieren, damit validierende Resolver authentifizierte Antworten zurückgeben — Anbieter wie Cloudflare aktivieren das mit einem Klick, während manche (darunter Vercel DNS) es überhaupt nicht unterstützen.
blume audit --url <origin> prüft das für dich: Wenn deployment.site gesetzt ist, fragt die Netzwerk-Stufe den Einstiegspunkt per DNS-over-HTTPS ab und meldet den exakt zu veröffentlichenden Record, falls keiner existiert, sowie ob die Antworten DNSSEC-authentifiziert sind. Setze BLUME_DOH_URL, um die Abfrage auf deinen eigenen Resolver zu richten, falls dein Netzwerk die öffentlichen (Google, Cloudflare) blockiert.
Web Bot Auth
Web Bot Auth wirkt in die andere Richtung: Es geht nicht darum, dass Agenten deine Docs lesen, sondern darum, dass die Agenten deiner Organisation sich ausweisen, wenn sie anderswo Requests stellen. Deine Agenten signieren ihre Requests mit HTTP Message Signatures, und empfangende Sites verifizieren sie gegen ein auf deiner Domain veröffentlichtes Public-Key-Verzeichnis. Wenn deine Organisation Agenten betreibt und deine Blume-Site unter der Domain liegt, als die sie sich ausweisen, veröffentliche deren öffentliche Schlüssel:
agents: {
webBotAuth: {
keys: [{ kty: "OKP", crv: "Ed25519", x: "JrQLj5P_89iXES9-vFgrIy29c…" }],
},
}
Blume liefert das JWKS dann unter /.well-known/http-message-signatures-directory mit seinem registrierten Medientyp auf jeder Build-Oberfläche aus. Das Verzeichnis ist per Definition öffentlich, deshalb lässt die Konfiguration nur öffentliche Schlüssel zu — ein JWK, das privates Material enthält (d, p, q, …), scheitert an der Validierung mit einem Fehler, statt ein geleaktes Credential auszuliefern. Erzeuge ein Ed25519-Paar mit:
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" })))'
Das öffentliche JWK kommt in die Konfiguration oben; das private dorthin, wo dein signierender Agent läuft (ein Secret Manager, niemals das Repo). Wenn deine Organisation keine Agenten betreibt, überspring das — ein leeres Verzeichnis bewirbt nichts, das der Verifikation wert wäre.
Da blume.config.ts zur Build-Zeit ausgeführt wird, muss der Schlüssel nicht fest einkodiert sein — lade ihn aus einer Build-Zeit-Umgebungsvariable, um die Konfiguration frei von Schlüssel-Blobs zu halten und ohne Commit zu rotieren:
const webBotAuthKey = process.env.WEB_BOT_AUTH_PUBLIC_JWK;
export default defineConfig({
agents: {
webBotAuth: {
keys: webBotAuthKey ? [JSON.parse(webBotAuthKey)] : [],
},
},
});
Umgebungen ohne die Variable veröffentlichen kein Verzeichnis, und ein so geladener Schlüssel wird exakt wie ein inline angegebener validiert — einschließlich der Prüfung auf privates Material. (Der öffentliche Schlüssel ist kein Geheimnis, ihn inline zu committen ist also genauso in Ordnung; die Env-Variable ist eine Frage der Ergonomie, nicht der Sicherheit.)