The only TypeScript toolkit for Agentic Resource Discovery (ARD). The spec's reference tooling is a Python conformance CLI; if you're shipping a site or MCP server from Node, this is the side you were missing. Zero runtime dependencies, ESM + CJS, Node 18+.
npx ai-catalog validate ./.well-known/ai-catalog.jsonerror /entries/0/type required `type` is missing — `mediaType` was renamed to `type` (ADR-0014)
That rename (ADR-0014) is one the spec's own shipped example still gets wrong. The validator tracks the normative spec text, so it catches the things a hand-edited catalog drifts on.
ARD lets a website advertise its AI agents, MCP servers, skills, and datasets at /.well-known/ai-catalog.json so agents can find them before invocation. This package is the TypeScript side: typed builders, a spec-aware validator with precise error paths, the full agent-driven discovery walk, and nested-catalog resolution.
npm install ai-catalognpx ai-catalog validate ./.well-known/ai-catalog.jsonerror /entries/0/type required `type` is missing — `mediaType` was renamed to `type` (ADR-0014)
warn /entries/1/identifier should use urn:air:{publisher}:{namespace}:{name} for open/federated use
The validator splits the spec's normative MUST rules (missing required members, the exactly-one-of url/data rule, identifier/version uniqueness, trust-identity domain alignment) from SHOULD/RECOMMENDED rules (urn:air identifiers, known media types, RFC 3339 timestamps), so you see error vs warn instead of one undifferentiated wall. Pass --strict to fail on warnings too.
Point it at a live one — Hugging Face (an ARD co-author) publishes a catalog you can validate today:
npx ai-catalog discover https://huggingface.co
npx ai-catalog validate <(curl -s https://huggingface.co/.well-known/ai-catalog.json)It passes, with one warn: their registry entry uses application/ai-registry+json, which isn't in the spec's recommended type set. That's the point of the split — a real-world file that's correct on every MUST but carries a media type the spec doesn't enumerate shows up as a note, not an error.
discover runs the spec's agent-driven procedure in order — HTTP Link header, then HTML <link rel="ai-catalog">, then the /.well-known/ fallback — and tells you which one hit:
npx ai-catalog discover https://example.comimport { discover } from "ai-catalog";
const { url, via, catalog } = await discover("https://example.com");
// via: "link-header" | "html-link" | "well-known"
console.log(`${catalog.entries.length} entries, found via ${via}`);import { CatalogBuilder, KnownType, buildAirUrn } from "ai-catalog";
const catalog = new CatalogBuilder({
displayName: "Acme Services",
identifier: "did:web:acme.com",
})
.addEntry({
identifier: buildAirUrn("acme.com", "mcp", "weather"),
type: KnownType.McpServerCard,
description: "Weather lookup MCP server.",
tags: ["weather", "mcp"],
url: "https://acme.com/.well-known/mcp/weather.json",
})
.addNestedCatalog("urn:air:acme.com:catalog:ml", "https://acme.com/ml/ai-catalog.json")
.validateOrThrow(); // throws with every problem listed, or returns the catalog
import { writeFileSync } from "node:fs";
writeFileSync(".well-known/ai-catalog.json", JSON.stringify(catalog, null, 2));A catalog can point at child catalogs (type: application/ai-catalog+json). resolveCatalog walks the tree, flattens every leaf entry, tags each with the depth and source URL it came from, follows the spec's depth limit of 4, and won't loop on a cycle:
import { resolveCatalog, selectLatest, resolveDisplayName } from "ai-catalog";
const entries = await resolveCatalog("https://acme.com/.well-known/ai-catalog.json");
for (const e of entries) {
console.log(e.depth, resolveDisplayName(e), e.type);
}
// Multi-version entries: pick the newest by SemVer, then updatedAt.
const latest = selectLatest(entries, "urn:air:acme.com:agent:finance");CLI equivalent:
npx ai-catalog resolve https://acme.com --names| Export | What it does |
|---|---|
validateCatalog(doc, { strict? }) |
Structural validation → { valid, errors, warnings } with JSON-pointer paths |
assertValidCatalog(doc) |
Throwing validator; narrows the type to AiCatalog |
CatalogBuilder / defineCatalog(host?) |
Chainable typed builder |
discover(siteUrl, opts?) |
Agent-driven discovery (Link header → HTML link → well-known) |
fetchCatalog(url, fetch?, timeoutMs?) |
Fetch + validate a catalog from a URL |
resolveCatalog(rootOrUrl, opts?) |
Walk nested catalogs, flatten to leaf entries |
resolveDisplayName(entry, referencedName?) |
Spec display-name resolution order |
selectLatest(entries, identifier) |
Newest entry among shared-identifier versions |
parseAirUrn / buildAirUrn / isAirUrn / identifierTail |
urn:air helpers |
parseLinkHeader / parseHtmlLink |
Standalone discovery parsers |
KnownType, WELL_KNOWN_PATH, CATALOG_MEDIA_TYPE, SPEC_VERSION |
Spec constants |
All functions that fetch accept an opts.fetch so you can inject a mock, a proxy, or an authenticated client; on Node 18+ the global fetch is the default.
Tracks ARD v1.0 as published in Agent-Card/ai-catalog, including ADR-0014 (mediaType → type) and the depth-4 nesting recommendation. ARD is a v0.9-era draft and still moving; if a field name shifts under you, open an issue.
Apache-2.0