Skip to content
Blume
English
Esc
navigateopen⌘Jpreview
On this page

Agent discovery

How agents find your machine-readable surface without guessing — the agent-readability manifest, Link headers, the RFC 9727 API catalog, WebMCP, published skills, DNS-based discovery, and Web Bot Auth keys.

Publishing llms.txt, Markdown mirrors, a JSON API, and an MCP server is only half the job — an agent still has to find them. Blume advertises the whole surface through the conventions agents actually probe: a manifest at the site root, Link headers and <link> tags, well-known files, and the browser’s own model context. Everything here is on by default and derived from what you’ve already enabled.

Agent readability

Blume writes an /agent-readability.json manifest at your site root that indexes the agent-facing surface described across this section — so an agent can discover it in a single fetch instead of guessing at conventions or scraping HTML. Like llms.txt, it’s on by default:

seo: {
  agentReadability: true,
}

The manifest lists only what you’ve enabled — the raw Markdown mirror pattern, the JSON API and its OpenAPI description, llms.txt and llms-full.txt, the MCP server and its discovery document, the Ask AI endpoint, the sitemap, and RSS feeds — alongside your site name, description, source repository, and the content-signal usage policy. URLs are absolute when deployment.site is set and root-relative otherwise:

{
  "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@1.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"
}

The contentNegotiation field appears only when the deployed site actually honors the Accept: text/markdown header — see content negotiation; on every other deployment the manifest advertises just the .md mirror pattern.

Set seo.agentReadability to false to skip it, or ship your own public/agent-readability.json to take over — Blume never overwrites a file you place in public/.

Agents that probe a site don’t know to look for the manifest — so Blume also advertises it in an RFC 8288 Link response header on the homepage, using IANA-registered relation types:

Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+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"

Each entry appears only when its feature is on. The alternate link points at the homepage’s Markdown mirror — the page’s own raw Markdown when the home route is a content page, or the synthesized llms.txt fallback when it’s a landing page. The service-desc link (RFC 8631) points at the JSON API’s OpenAPI description, and api-catalog at the generated API catalog. The header rides on every surface Blume controls: the dev server (check it with curl -I localhost:4321), static builds via the emitted _headers file (Netlify and Cloudflare), and Vercel server builds via the deploy’s routing rules.

Not every agent enters through the root, though — one following a search result or a shared link lands on a deep page and never sees the homepage header. So every rendered page also carries the same discovery links in its HTML <head>, using the same IANA-registered relations:

<link
  rel="describedby"
  href="/agent-readability.json"
  type="application/json"
/>
<link rel="describedby" href="/llms.txt" type="text/plain" />
<link rel="alternate" href="/docs/example.md" type="text/markdown" />

Here the alternate link points at that page’s own raw-Markdown mirror, so an agent can jump straight from the HTML it landed on to the token-efficient version. Because the head links travel with the prerendered HTML, they also work on hosts that ignore _headers and can’t send custom response headers at all (GitHub Pages, S3) — no matter which page the agent enters on.

API catalog

When the site publishes APIs, Blume generates an RFC 9727 API catalog at /.well-known/api-catalog — a linkset that lets agents enumerate your APIs from the domain alone, served with its registered application/linkset+json media type on every build surface. There’s nothing to configure: the catalog is derived from what’s already in blume.config.ts. Each OpenAPI or AsyncAPI reference becomes an entry anchored at its rendered docs route, with service-doc pointing at those docs and service-desc at the spec when it lives at a fetchable URL; the site’s own JSON API becomes an entry described by its /openapi.json; and the MCP server becomes an entry with its discovery document as the service description:

{
  "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" }
      ]
    }
  ]
}

A site with no API references, no MCP server, and the JSON API turned off emits no catalog — there’d be nothing in it. As everywhere, a public/.well-known/api-catalog file you ship yourself wins over the generated one.

WebMCP

WebMCP is an emerging browser API that lets a page register tools directly with an agentic browser — no separate server connection needed. Every Blume page registers the docs’ read-only surface on the page’s model context: search_docs (site search), get_page (a page’s raw Markdown), and list_pages (the llms.txt index). The script is tiny, loads no search machinery until a tool is actually called, and silently no-ops in every browser without the API — which today is all of them outside Chrome’s early preview. It registers on whichever surface the in-flux spec exposes (navigator.modelContext or document.modelContext), via provideContext or per-tool registerTool.

It’s on by default; set webmcp: false to opt out:

ai: {
  webmcp: false,
}

Skills discovery

If your project ships agent skills — the Blume repo itself does — point ai.skills at the directory that holds them, and the build publishes them for discovery per the Agent Skills Discovery RFC:

ai: {
  skills: "./skills",
}

The path resolves against your project root, and each subdirectory with a SKILL.md becomes a published skill. A skill that’s a lone SKILL.md is copied verbatim to /.well-known/agent-skills/<name>/SKILL.md (type: "skill-md"); a skill with supporting resources (scripts/, references/, assets/) is bundled into a deterministic .tar.gz (type: "archive") so its relative references resolve after unpacking, with script execute bits preserved. The discovery index at /.well-known/agent-skills/index.json carries the v0.2.0 $schema and, per skill, its name, type, description (from the SKILL.md frontmatter), artifact URL, and the SHA-256 digest clients verify downloads against.

Skills with a missing or spec-invalid name/description are skipped with a build warning rather than published broken, and a public/.well-known/agent-skills/index.json you ship yourself takes over the whole surface. Published skills are also listed in llms.txt.

DNS-based discovery (DNS-AID)

DNS for AI Discovery is an emerging IETF draft that lets agents discover a site’s AI surface before making a single HTTP request, by querying ServiceMode SVCB/HTTPS records at a well-known DNS entrypoint. DNS records live in your zone, not in the build, so this is the one discovery surface Blume can’t publish for you — instead, add a record with your DNS provider:

_index._agents.docs.example.com. 3600 IN HTTPS 1 docs.example.com. alpn=h2

Use the HTTPS record type if your provider offers it (Vercel DNS does; it doesn’t support the plain SVCB type), or a ServiceMode SVCB record with alpn and port parameters otherwise. The draft also recommends signing the zone with DNSSEC so validating resolvers return authenticated answers — providers like Cloudflare enable it in one click, while some (including Vercel DNS) don’t support it at all.

blume audit --url <origin> checks this for you: when deployment.site is set, the network tier queries the entrypoint over DNS-over-HTTPS and reports the exact record to publish if none exists, plus whether the answers are DNSSEC-authenticated. Set BLUME_DOH_URL to point the lookup at your own resolver if your network blocks the public ones (Google, Cloudflare).

Web Bot Auth

Web Bot Auth works in the other direction: it’s not about agents reading your docs, but about your organization’s agents identifying themselves when they make requests elsewhere. Your agents sign their requests with HTTP Message Signatures, and receiving sites verify them against a public-key directory published on your domain. If your org runs agents and your Blume site lives at the domain they identify as, publish their public keys:

ai: {
  webBotAuth: {
    keys: [{ kty: "OKP", crv: "Ed25519", x: "JrQLj5P_89iXES9-vFgrIy29c…" }],
  },
}

Blume then serves the JWKS at /.well-known/http-message-signatures-directory with its registered media type on every build surface. The directory is public by definition, so the config only admits public keys — a JWK containing private material (d, p, q, …) fails validation with an error rather than shipping a leaked credential. Generate an Ed25519 pair with:

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" })))'

The public JWK goes in the config above; the private one goes wherever your signing agent runs (a secret manager, never the repo). If your organization doesn’t operate agents, skip this — an empty directory advertises nothing worth verifying.

Since blume.config.ts is executed at build time, the key doesn’t have to be hardcoded — load it from a build-time environment variable to keep the config free of key blobs and rotate without a commit:

const webBotAuthKey = process.env.WEB_BOT_AUTH_PUBLIC_JWK;

export default defineConfig({
  ai: {
    webBotAuth: {
      keys: webBotAuthKey ? [JSON.parse(webBotAuthKey)] : [],
    },
  },
});

Environments without the variable publish no directory, and a key loaded this way is validated exactly like an inline one — including the private-material check. (The public key isn’t a secret, so committing it inline is equally fine; the env var is an ergonomic choice, not a security one.)

Was this page helpful?