A Go toolkit for consuming and validating AI Catalog documents, the JSON format for making AI artifacts (MCP servers, A2A agents, datasets, model cards, nested catalogs, …) discoverable.
It implements the AI Catalog specification.
The repository follows the spec's split between normative and non-normative content:
- Normative: the stable, supported API. Document types, parsing and querying (
catalog), conformance validation (validate), and digest verification and canonicalization (trust).catalogalso has lookups the spec does not define, such asGetByTag,GetByPublisherandSearchByRegex. - Non-normative:
providerand thecatalog.Sourceinterface load a catalog from a local file or an HTTP endpoint.examples/holds reference code, such as packaging a catalog as an OCI artifact. It is not part of the supported API.
go get github.com/Agent-Card/ai-catalog-goThe minimum Go version is declared in go.mod.
The provider package returns a catalog.Source. Nested catalog entries are not resolved; follow them yourself.
import (
"context"
"github.com/Agent-Card/ai-catalog-go/catalog"
"github.com/Agent-Card/ai-catalog-go/provider"
)
ctx := context.Background()
// From a local file:
src, err := provider.JSON("ai-catalog.json")
// From an explicit URL:
src, err = provider.Web(ctx, "https://acme-corp.com/catalogs/finance.json")
// From a domain's well-known URI (RFC 8615):
src, err = provider.Web(ctx, "https://acme-corp.com"+catalog.WellKnownPath)Web fetches over HTTP. Use provider.WithHTTPClient(myClient) for a custom client or provider.WithFetcher(...) for a custom transport.
A parsed *catalog.AICatalog needs no Source; call its methods directly.
Source.Load returns the whole catalog as a *catalog.AICatalog. Its query methods cover that document's entries only:
doc, err := src.Load(ctx)
if err != nil {
// handle load error
}
entry, ok := doc.GetByID("urn:air:acme-corp.com:mcp:weather")
mcpServers := doc.GetByType(catalog.MediaTypeMCPServerCard)
hits := doc.Search("weather")
matched, err := doc.SearchByRegex(`^urn:air:acme-corp\.com:`)The same methods work on any parsed document:
doc, _ := catalog.ParseFile("ai-catalog.json")
entry, ok := doc.GetByID("urn:air:acme-corp.com:mcp:weather")
agents := doc.GetByType(catalog.MediaTypeA2AAgentCard)
byTag := doc.GetByTag("finance")
byPublisher := doc.GetByPublisher("did:web:acme-corp.com")Entries may share an identifier and differ by version:
all := doc.Versions("urn:air:acme.com:agent:finance") // every version
v2, ok := doc.GetByIDAndVersion("urn:air:acme.com:agent:finance", "2.0.0")
latest, ok := doc.GetLatest("urn:air:acme.com:agent:finance") // semver, then updatedAtGetLatest prefers entries with a valid semantic version, compared by semver and then updatedAt. Without one it uses the newest updatedAt.
Returns the entry's displayName, or else the last segment of its identifier.
name := entry.ResolveDisplayName()
// "urn:air:acme-corp.com:mcp:weather" -> "weather"import "github.com/Agent-Card/ai-catalog-go/validate"
result := validate.Validate(doc)
if !result.IsValid {
for _, d := range result.Errors {
log.Printf("%s %s: %s", d.Code, d.Path, d.Message)
}
}
log.Printf("conformance level: %s", result.ConformanceLevel) // minimal | discoverable | trusted
// Validate whatever a Source is backed by:
result, err := validate.Source(ctx, src)
// Fix the clock used for expiry, or the nesting limit (default 4):
result = validate.Validate(doc, validate.WithClock(clock), validate.WithMaxNestingDepth(2))Diagnostics carry a stable Code; match on it rather than on the message.
Trusted is structural. Every root entry's trust manifest must be signed, bound to its entry by subject, unexpired, and conform to the did:web Publisher Profile: a urn:air identifier, identity exactly did:web:{publisher}, an ES256 signature whose kid is the identity plus a fragment, and no jku, jwk, x5u or x5c. Profile violations are warnings that keep the catalog at Discoverable. Signatures are not verified and DID documents are not resolved.
import "github.com/Agent-Card/ai-catalog-go/trust"
// Verify an attestation digest against its bytes:
ok, err := trust.VerifyDigest("sha256:9f86d0...", data)
// Canonicalize a manifest (JCS, RFC 8785) for signing or verification:
canonical, err := trust.CanonicalizeTrustManifest(entry.TrustManifest)A manifest read from a document is canonicalized from its original bytes, so members the SDK does not model are covered as published. A manifest built in code or changed after reading is canonicalized from its serialization; sign what CanonicalizeTrustManifest returns for it. TrustManifest.Raw() returns the original bytes.
Verify signatures against the original bytes, not a re-serialized document, or unmodeled members drop out of the payload. The built-in providers keep those bytes and expose them through catalog.RawSource:
if rawSource, ok := src.(catalog.RawSource); ok {
raw, err := rawSource.Raw(ctx)
// Strips the top-level "signature" and canonicalizes the rest:
payload, err := trust.CanonicalizeForSignature(raw)
}OCI packaging is not part of the specification, so it lives in examples/oci:
go run ./examples/ociThis repository uses Task:
task test # run unit tests with race detector and coverage
task lint # run golangci-lintSee CONTRIBUTING.md and CODE_OF_CONDUCT.md.
Apache-2.0. See LICENSE.