Cheela Labs
← BACK TO BLOG
35 MIN READViren Tanti

Agent Discovery Spec: a .well-known for AI agents

ADS is an open, vendor-neutral capability manifest for AI agents, served at /.well-known/agent-discovery.json. Deep dive, worked examples, and how to contribute.

Every AI provider, runtime, and agent framework has grown its own private answer to a single question: "What can you do, and how do I call it?"

Model vendors ship tool schemas. Protocol servers list their tools over their own wire format. Every framework invents its own registration shape. None of them agree — and critically, none of them are designed to be read before you've already committed to that vendor's SDK.

The Agent Discovery Specification (ADS) is a small, MIT-licensed, provider-agnostic spec that fixes exactly one part of that problem. This is a full walkthrough of what it defines, the design decisions worth stealing even if you never adopt it, how to actually use it on both the server and client side, and how to contribute to it.

TL;DR ADS defines a capability manifest — a versioned JSON document describing what an agent-addressable system can do — plus the discovery flow for finding it, primarily at GET /.well-known/agent-discovery.json. It deliberately does not define how you invoke a capability once you've found it. Current version: 0.1.0 (Draft). Think /.well-known/openid-configuration, but for agents.

New to this? Start with How AI agents find out what other systems can do — the same ideas without the schema detail, plus a 10-minute build-it-yourself tutorial. This page assumes you're comfortable with JSON Schema and HTTP.

Left: four clients wired to four systems through four different private capability formats, producing sixteen bespoke integrations. Right: every system publishes one ADS manifest and every client reads the same document.


Table of contents


The problem ADS actually solves

Agent-to-agent and agent-to-tool integration currently scales as N × M. Every client that wants to talk to every system writes a bespoke integration against whatever private format that system happens to expose.

That's tolerable when N and M are small and you own both ends. It stops being tolerable the moment you want any of these:

  • An orchestrator that can route to systems it wasn't built against.
  • A registry that can crawl and index capabilities without a per-vendor adapter.
  • An agent that can be handed a domain name and figure out the rest.
  • A CI check that fails when a downstream capability changes its contract.

The specific gap ADS targets is pre-connection discovery. Most existing mechanisms answer "what tools do you have?" only after you've picked a protocol, imported an SDK, opened a session, and authenticated. ADS moves that question earlier: to a plain, cacheable, unauthenticated GET.


What is the Agent Discovery Specification?

The Agent Discovery Specification (ADS) is an open, provider-agnostic standard that defines two things: a JSON capability manifest describing what an agent-addressable system can do, and the discovery flow a client follows to find and validate that manifest — primarily by fetching https://{host}/.well-known/agent-discovery.json.

Everything else is explicitly out of scope. From spec/01-overview.md, ADS is:

  • Not an invocation protocol. It points at an endpoint; the transport named there owns the request/response contract.
  • Not an auth protocol. A manifest declares that a capability needs oauth2, not how to complete the flow.
  • Not a model or agent framework. It doesn't care whether a capability is backed by an LLM, a deterministic function, a human in the loop, or another agent.
  • Not a registry or marketplace. It defines how you resolve one system's manifest, not how manifests get listed or ranked in aggregate.

The spec's own summary of its scope is the sharpest line in the document:

ADS answers "what exists and how do I start," not "how do I finish."

Design goals

Goal What it means in practice
Provider agnostic Says nothing about which model or vendor sits behind a capability
Runtime agnostic Works for one serverless function or a fleet behind a load balancer
Framework agnostic Doesn't assume any orchestration library — any of them can be described by a manifest
Open source MIT, no CLA, no trademark gate on implementations
Community driven Changes go through a public proposal process, not one vendor's roadmap
Extensible New categories and vendor data attach without touching core or bumping MAJOR
Versioned Both the spec and every manifest instance carry an explicit version

Conformance levels

There are exactly two, and there is deliberately no middle ground:

  • Core-conformant — implements everything in the ADS Core document set, no extension blocks. A client that only understands Core MUST be able to fully parse and act on such a manifest.
  • Extended — Core-conformant plus namespaced extension blocks. A client that doesn't understand a given extension MUST still be able to use every Core field normally.

There is no "partial Core conformance." An implementation either satisfies every MUST in ADS Core or it isn't ADS-conformant.

That strictness is the point. The one thing ADS promises — you can always at least discover something usable — stops being true the moment "required" fields become negotiable.


ADS vs MCP vs OpenAPI vs A2A

This is the question anyone evaluating ADS asks first, so let's be precise. ADS is a layer, not a competitor to any of these.

A layered diagram: layer zero is the entry point, a URL or domain. Layer one is discovery — this is ADS. Layer two is invocation, owned by OpenAPI, MCP, gRPC or queues, and explicitly out of ADS scope.

Answers When you can read it Scope
ADS What capabilities exist, what they need, where to call them Before you pick an SDK or open a connection Discovery only
MCP What tools a server exposes After you're connected and speaking MCP Full protocol: discovery and invocation
OpenAPI Every detail of an HTTP API's surface Wherever the document is published HTTP APIs specifically
A2A Agent Card An agent's identity, skills, and endpoint Before connecting, at its own well-known path Agent-to-agent interop
llms.txt Which prose docs an LLM should read Before connecting Content, not callable capabilities

Three things follow from that table:

  1. ADS and MCP are complementary, not alternatives. A perfectly reasonable ADS manifest has "transport": "mcp" and an address pointing at an MCP server. ADS gets you to the door; MCP is what you speak once inside.
  2. ADS and OpenAPI are complementary too. OpenAPI describes an HTTP API exhaustively. ADS is the thin index that says "this system has a capability called com.example.lookupOrder, version 1.2.0, and here's the HTTP address" — then gets out of the way.
  3. A2A's Agent Card is the closest genuine neighbour. It also uses a well-known URI (RFC 8615 style) and also describes capabilities before connection. The difference is scope and framing: the Agent Card is part of a specific agent-to-agent protocol, while ADS is deliberately protocol-neutral and describes anything agent-addressable — a single serverless function, a tool server, a runtime fleet. Whether the ecosystem needs both is a fair question, and one worth raising as an issue rather than assuming.

Anatomy of a capability manifest

The minimal valid manifest is five fields:

{
  "specVersion": "0.1.0",
  "id": "com.example.minimal",
  "name": "Minimal Example",
  "provider": { "name": "Example Inc." },
  "capabilities": []
}

Yes, capabilities MAY be empty — that's a legal state for a system temporarily offering nothing, which is a nicer failure mode than a 404 or a malformed document.

Here's the fully-populated version, annotated:

An annotated ADS manifest showing the five required top-level fields alongside the optional description, discovery, extensions and lastUpdated fields, with callouts explaining each.

Top-level fields

Field Type Required Notes
specVersion SemVer string Yes Which ADS Core this manifest conforms to
id string Yes Stable identity. SHOULD be reverse-DNS or a URN. MUST NOT change across revisions
name string Yes Human-readable display name
description string No Human-readable summary
provider object Yes Who operates the system
capabilities array Yes MAY be empty; SHOULD be non-empty in normal operation
discovery object No Caching and integrity metadata about the manifest itself
extensions object No Namespaced, additive data outside Core
lastUpdated RFC 3339 date-time No Advisory only — clients MUST NOT treat manifests as stale based on this alone

Two of those deserve a closer look.

provider is not the model provider. This is called out as normative in the terminology section, and it's the kind of ambiguity that would otherwise produce two incompatible reading of the same field. provider is whoever operates the system. If your support agent runs on someone else's LLM, provider.name is still your company. Model details, if the client needs them, go in capability metadata or an extension block.

id is a trust anchor, not a label. It's how a client tells "same system, new manifest" from "different system." The backward-compatibility document treats reusing an id for a materially different system as a violation regardless of version number — it breaks identity assumptions, not just schema assumptions.

capabilities[]

Field Required Notes
name Yes Namespaced, reverse-DNS style: com.example.lookupOrder
version Yes SemVer for this capability's contract, independent of specVersion
description No Human-readable summary
inputSchema No JSON Schema. Absence means "consult the transport docs" — not "no input"
outputSchema No JSON Schema for the output shape
endpoint Yes Where and how to invoke
deprecated No Defaults to false
deprecatedSince Required if deprecated: true The capability version at which deprecation was announced
removalNotBefore No Earliest capability version at which removal is legal

That inputSchema note is a small piece of excellent spec-writing. "Field absent" and "field present but empty" mean genuinely different things, and saying so prevents a whole class of client bugs where a missing schema is silently read as "takes no arguments."

endpoint

{
  "transport": "http",
  "address": "https://api.example.com/v1/orders/lookup",
  "auth": "apiKey",
  "authDetails": { "headerName": "X-Api-Key" }
}
  • Well-known transport values: http, grpc, mcp, queue
  • Well-known auth values: none, apiKey, oauth2, mtls

Both are open strings, not closed enums — a new transport never requires a Core version bump. Custom auth schemes MUST carry a custom: prefix (custom:hmac-sig) so a client can distinguish "an auth scheme I don't recognise" from "a typo of one I do." That's a genuinely thoughtful detail.

auth is required. There's no default and no omitting it — a capability with no authentication has to say "auth": "none" out loud. That makes "unauthenticated" a deliberate, auditable declaration rather than something you infer from a missing field. Small decision, real security value.


The discovery flow

Three mechanisms

1. HTTP well-known URI (the primary one).

GET https://{host}/.well-known/agent-discovery.json

Requirements worth noting:

  • MUST be served as application/json (or a +json structured suffix)
  • MUST validate against the normative JSON Schema
  • SHOULD support ETag / Cache-Control / If-None-Match
  • SHOULD answer HEAD for cheap freshness checks
  • SHOULD send Access-Control-Allow-Origin: *"a manifest is a public discovery document, not a protected resource"

That CORS line is the correct call and it's good that it's explicit. Discovery documents that browser clients can't read are half-useless.

2. DNS TXT record — for discovery before any HTTP connection exists:

_agent-discovery.example.com.  IN  TXT  "ads=https://example.com/.well-known/agent-discovery.json"

It's a pointer, not the manifest — TXT size limits make embedding impractical.

3. Inline/local — a plain agent-discovery.json file for CLI tools, libraries, and embedded configs with no network address at discovery time. The spec standardises the filename convention and nothing more, which is the right amount of standardising for something inherently context-specific.

The seven steps

The ADS resolution and validation flow: fetch, parse, validate, version-check, optionally verify the signature, select a capability, then invoke. Parse, schema and major-version failures stop discovery; an unrecognised transport or auth value only skips that one capability.

sequenceDiagram
    participant Client
    participant System

    Client->>System: GET /.well-known/agent-discovery.json
    System-->>Client: 200 OK, application/json

    Note over Client: 1. Parse JSON
    Note over Client: 2. Validate against manifest.schema.json
    Note over Client: 3. Check specVersion compatibility
    Note over Client: 4. (optional) Verify discovery.signature
    Note over Client: 5. Select capability by name + version range
    Note over Client: 6. Ignore capabilities with unrecognised transport/auth

    Client->>System: Invoke capabilities[i].endpoint (out of ADS scope)

Error handling

The error table is where the spec's temperament shows most clearly:

Situation Client behaviour
404 on the well-known path Not an error. The system just doesn't support ADS
Malformed JSON Discovery fails. No partial parsing, no regex-scraping fields out
Valid JSON, fails schema Discovery fails. No best-guess field extraction
Unrecognised specVersion MAJOR Fails with a clear incompatibility signal — not a silent no-op
Some capabilities have unknown transport/auth Discovery succeeds. Those specific capabilities are simply unusable
Signature present, verification fails Client-defined policy — but proceeding anyway SHOULD be deliberate and loggable

Note the shape of that: hard-fail on anything ambiguous, soft-skip on anything merely unknown. A malformed document stops you cold; an unfamiliar transport costs you one capability. That asymmetry is the whole design in miniature.

Manifest integrity

discovery.signature is optional, minimal (algorithm + value + keyUrl), and exists for one scenario: you received the manifest through something you don't fully trust — a cache, a registry mirror, a forwarded copy.

The signature is computed over the manifest with the discovery.signature field removed, because you can't sign a document containing its own signature. Key rotation and trust-on-first-use are explicitly left to implementations.

⚠️ This is where I'd push hardest as a contributor. "Sign the document minus the signature field" is under-specified without a canonicalisation rule. Two conformant servers can serialise the same logical JSON with different key ordering, whitespace, or Unicode escaping, and produce different signatures over identical content. RFC 8785 (JSON Canonicalization Scheme) exists precisely for this. See open questions.


Three version numbers, three questions

Most specs that get versioning wrong get it wrong by conflating things. ADS separates three numbers and gives each its own "what bumps this" table.

Three cards: spec version answers which ADS Core a manifest follows; capability version answers whether one capability's contract changed; proposal number ADS-N is a permanent identifier, not a version.

1. specVersion — which ADS Core does this manifest follow?

Change Bump
Remove or rename a required field MAJOR
Change the meaning of an existing field MAJOR
Change the discovery flow so old clients can't follow MAJOR
Add a new optional field MINOR
Add a new well-known transport or auth value MINOR
Clarify prose without changing behaviour PATCH
Fix a schema bug that rejected valid manifests PATCH
Fix a schema bug that accepted invalid manifests MINOR

That last row is the interesting one, and the spec calls it out rather than hiding it. Tightening validation is technically breaking for anyone accidentally relying on the old looseness — but it's needed to fix a real defect. These ship as MINOR with an explicit changelog entry and, if the blast radius looks nontrivial, a deprecation window instead of an immediate hard rejection.

2. capabilities[].version — did this capability's contract change?

Change Bump
Remove a required input, change output shape incompatibly, change behaviour MAJOR
Add an optional input, add output fields, widen accepted input MINOR
Fix a typo, clarify inputSchema without changing what validates PATCH

The guidance for clients is exactly right: pin a capability version range the way you'd pin a package dependency, and treat a MAJOR bump as "this needs re-integration," not "this will probably still work."

3. ADS-N — which change proposal?

Not a version at all. A permanent, sequential identifier assigned once on merge, the way an EIP or RFC number works. Never reused, even if the proposal is later withdrawn. One spec release may fold in zero, one, or several Final proposals.

A manifest never references a proposal number. Only specVersion.

The pre-1.0 escape hatch

While specVersion is 0.x, MINOR bumps MAY include small breaking changes — but only if no known implementation depends on the changed behaviour, and every such change MUST be flagged as breaking in the changelog. At 1.0.0 the exception ends. That's standard SemVer practice, stated explicitly rather than assumed.


Extensibility: the one load-bearing rule

Everything about how ADS stays small follows from a single sentence:

A Core-conformant client MUST NOT fail to process a manifest solely because it contains a field, capability, transport, auth value, or extensions entry it doesn't recognise. It ignores what it doesn't understand and uses what it does.

The schema enforces this structurally: additionalProperties: true appears at every level of manifest.schema.json. An over-strict validator that rejects unknown fields isn't being careful — it's non-conformant.

Three mechanisms make that rule safe to follow.

Namespacing

Every capabilities[].name and every extensions key MUST be namespaced: at least two dot-separated segments, reverse-DNS style.

  • The namespace portion compares case-insensitively, matching DNS semantics. Com.Example.foo and com.example.foo are the same namespace.
  • The leaf segment MAY use camelCase for readability — it carries no collision-avoidance weight.
  • core.* is reserved for categories ADS itself may standardise later. None exist yet.
  • If you control example.com, com.example.* is yours by construction. No registration required. Same trust model as Java package names.

The extensions block

Keys SHOULD be {namespace}/{extensionVersion}:

{
  "extensions": {
    "com.example.billing/1.0": { "tier": "enterprise", "rateLimitPerMinute": 600 },
    "com.example.billing/2.0": { "tier": "enterprise", "limits": { "rpm": 600 } }
  }
}

Two versions of the same extension, side by side, during a migration — and a client can tell which shape to expect without parsing the contents first. That's a small design decision with real operational payoff.

An extension's internal shape is owned by whoever owns the namespace, not by ADS. If one becomes widely used enough to standardise, it goes through the Extension track — and even then, extensions never gain required-field status.

The optional short-prefix registry

Reverse-DNS is collision-safe but verbose. For projects that want a memorable prefix, registry/namespaces.json is a first-come, first-served registry maintained by lightweight PR — no proposal cycle. It's currently seeded with exactly one entry.

Registering is entirely optional, and the registry README is refreshingly clear about what it doesn't do: it doesn't review what you build, doesn't grant trademark rights, and doesn't reserve the string outside ADS manifests.

What extensibility does not cover

Changing the meaning of an existing Core field, or promoting an optional field to required, is not an extension. It's a Core change, full proposal process, MAJOR bump. Extensibility adds; it doesn't route around the versioning rules.


Backward compatibility and the golden rule

A client that only reads the fields it already understood from a previous MINOR/PATCH version MUST continue to work correctly against every later MINOR/PATCH version within the same MAJOR.

Restated as a test: if shipping a change would make a well-behaved, already-deployed client start behaving incorrectly without that client changing anything, the change is MAJOR — no matter how small it looks in a diff.

Deprecating a capability

{
  "name": "com.example.legacyCancelOrder",
  "version": "1.0.0",
  "deprecated": true,
  "deprecatedSince": "1.0.0",
  "removalNotBefore": "2.0.0",
  "endpoint": { "transport": "http", "address": "…", "auth": "apiKey" }
}

The rules around this are stricter than most specs bother with:

  • deprecated: true MUST be accompanied by deprecatedSince — and the JSON Schema actually enforces this with a conditional if/then, so it's a validation error, not just a prose expectation.
  • A deprecated capability MUST remain fully functional. Deprecation is a signal, not a degradation. "Silently returning errors from a 'deprecated' capability is a spec violation."
  • The gap between deprecatedSince and removalNotBefore MUST be at least one full MAJOR of that capability. Deprecating in 1.3.0 means the earliest legal removal is 2.x — never a later 1.x.
  • Systems SHOULD also give a wall-clock window (six months is the suggested default), because version bumps and calendar time don't move together.

Serving multiple spec versions

/.well-known/agent-discovery.json        → latest supported specVersion
/.well-known/agent-discovery.v1.json     → pinned to the latest 1.x
/.well-known/agent-discovery.v2.json     → pinned to the latest 2.x

Optional. You may equally just run two MAJORs in parallel until old clients age out. Both are conformant; the spec correctly frames this as an operational decision, not a spec one.

Optional version negotiation

GET /.well-known/agent-discovery.json
Accept: application/json; ads-version=^1.2

A system that doesn't implement negotiation MUST ignore the parameter rather than reject the request. Negotiation is additive, never a gate.

(Implementation note: ^ is a valid tchar under RFC 9110, so a bare ^1.2 is a legal unquoted parameter value. Richer ranges with spaces or > characters would need quoting — ads-version=">=1.2 <2". Worth a clarifying sentence in the spec.)

Two things never allowed, at any version

  1. Reusing id for a materially different system.
  2. Silently changing what a capability does without bumping its version.

Both break the trust model rather than the schema, which is why no MAJOR bump launders them.


Governance: why it's modelled on EIPs

The governance model is lifted, deliberately, from Ethereum's EIP process and IETF's RFC process — specifically the part where "who can edit the document" is separated from "who decides what's true."

Editors maintain mechanics: assign numbers, check template compliance, merge editorial fixes, cut releases. And then the load-bearing sentence:

An editor can reject a proposal for being incomplete or malformed, never for being one they personally disagree with.

Implementers — anyone shipping something that reads or writes a manifest — don't need permission to implement, but proposals that would break existing implementations need sign-off from at least one affected implementer before moving past Review.

Disputes default to not shipping. If editors disagree on whether Last Call objections are resolved, the proposal stays in Last Call. There's no BDFL tiebreaker, by design: "a spec meant to outlive any one company shouldn't have a single point of failure in its decision process either."

There's also no conformance mark and no trademark gate. If your implementation satisfies the MUSTs, it's conformant, full stop.

The Cheela question

ADS lives under the Cheela Labs org, and Cheela is named as the first production implementer. The repo addresses the obvious conflict-of-interest question in two places rather than hoping nobody asks:

Cheela is the first production implementer […] but that's a statement about who built something on top of this spec first, not a statement about ownership. The spec is versioned, governed, and evolved independently, the way TC39 doesn't belong to V8 and OpenAPI doesn't belong to Swagger.

And in GOVERNANCE.md:

Editors who are also employed by an implementing company recuse their editor judgment (though not their implementer feedback) on proposals where their employer has a direct commercial interest in the outcome.

Stating a recusal rule up front is the right move. Whether it holds is something only the commit history will eventually prove — and that's fine, because the commit history is public.


Using the spec

Here's the practical part. Four things you'd actually do.

1. Publish a manifest (server side)

The simplest conformant implementation is a static file with the right headers. On Express:

import express from "express";
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";

const app = express();

const manifest = readFileSync("./agent-discovery.json");
const etag = `"${createHash("sha256").update(manifest).digest("base64url")}"`;

app.get("/.well-known/agent-discovery.json", (req, res) => {
  // A manifest is a public discovery document, not a protected resource.
  res.set("Access-Control-Allow-Origin", "*");
  res.set("Content-Type", "application/json");
  res.set("Cache-Control", "public, max-age=3600");
  res.set("ETag", etag);

  if (req.headers["if-none-match"] === etag) return res.status(304).end();
  res.send(manifest);
});

// SHOULD answer HEAD for cheap freshness checks — Express does this
// automatically for GET routes, but be explicit if you use a raw server.

app.listen(3000);

On a static host (S3, Cloudflare Pages, Netlify, GitHub Pages), you need three things:

  1. The file served at exactly /.well-known/agent-discovery.json — note the leading dot; some build tools strip dot-directories by default.
  2. Content-Type: application/json (not text/plain, which some hosts default to for unknown extensions — though .json is normally fine).
  3. Access-Control-Allow-Origin: *.

If your discovery.cacheTtlSeconds and your Cache-Control: max-age disagree, clients get to pick. Keep them in sync.

2. Validate it in CI

The schema is the authority for structure, so wire it into CI on day one:

npm i -D ajv-cli ajv-formats
// package.json
{
  "scripts": {
    "validate:manifest": "ajv validate --spec=draft2020 -c ajv-formats -s spec/schema/manifest.schema.json -d 'public/.well-known/agent-discovery.json'"
  }
}

Don't skip ajv-formats. In JSON Schema draft 2020-12, format is an annotation by default — a validator without format assertion enabled will happily accept "lastUpdated": "yesterday" and "url": "not a url". The manifest schema uses format on lastUpdated, provider.url, and signature.keyUrl, so assertion mode is what you want.

As a GitHub Action:

name: validate-manifest
on: [push, pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "22" }
      - run: npm ci
      - run: npm run validate:manifest

Same pattern in Python with jsonschema, Go with santhosh-tekuri/jsonschema, or whatever your stack uses — the schema is plain draft 2020-12.

3. Consume a manifest (client side)

This is the part where conformance bugs live. The single most common mistake will be failing on unknown values instead of skipping them.

type Manifest = {
  specVersion: string;
  id: string;
  name: string;
  provider: { name: string; url?: string; contact?: string };
  capabilities: Capability[];
  discovery?: { cacheTtlSeconds?: number; signature?: unknown };
};

type Capability = {
  name: string;
  version: string;
  endpoint: { transport: string; address: string; auth: string };
  deprecated?: boolean;
};

const SUPPORTED_MAJOR = 0;
const KNOWN_TRANSPORTS = new Set(["http", "mcp"]); // only what WE can speak
const KNOWN_AUTH = new Set(["none", "apiKey", "oauth2"]);

export async function discover(origin: string): Promise<Manifest | null> {
  const url = new URL("/.well-known/agent-discovery.json", origin);
  const res = await fetch(url, { headers: { accept: "application/json" } });

  // 404 is "doesn't support ADS", not an error worth alarming on.
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`discovery failed: HTTP ${res.status}`);

  // Malformed JSON => discovery fails. No partial recovery.
  const manifest = (await res.json()) as Manifest;

  // Validate against manifest.schema.json here with your validator of choice.
  // A missing required Core field is not a partial manifest — it's not a manifest.

  const major = Number(manifest.specVersion.split(".")[0]);
  if (Number.isNaN(major) || major > SUPPORTED_MAJOR) {
    throw new Error(`incompatible ADS specVersion: ${manifest.specVersion}`);
  }

  return manifest;
}

export function selectCapability(
  manifest: Manifest,
  name: string,
  satisfies: (version: string) => boolean,
): Capability | undefined {
  return manifest.capabilities.find((c) => {
    if (c.name.toLowerCase() !== name.toLowerCase()) return false;
    if (!satisfies(c.version)) return false;

    // Skip — do NOT throw — on transports and auth schemes we don't speak.
    if (!KNOWN_TRANSPORTS.has(c.endpoint.transport)) return false;
    if (!KNOWN_AUTH.has(c.endpoint.auth)) return false;

    if (c.deprecated) console.warn(`[ads] ${c.name} is deprecated`);
    return true;
  });
}

Four details in there worth calling out, because each maps to a normative MUST:

  • 404 returns null, not an exception. "Unsupported" is not "broken."
  • An unrecognised MAJOR throws with a clear signal, rather than optimistically proceeding.
  • Unknown transport/auth filters the capability out. Failing here is the conformance bug the spec names explicitly.
  • Capability name matching is case-insensitive, because the namespace portion compares case-insensitively per DNS semantics. (Strictly, only the namespace portion should be case-folded — the leaf may be camelCase and meaningful. Folding the whole string is the pragmatic approximation; folding nothing is the bug.)

Finally: do not cache a manifest forever. Capabilities can appear, disappear, or change version between fetches. Treating a manifest as permanent truth is, in the spec's words, a client bug.

4. Wire up DNS discovery

_agent-discovery.example.com.  IN  TXT  "ads=https://example.com/.well-known/agent-discovery.json"

Verify it:

dig +short TXT _agent-discovery.example.com

Worth understanding the trust boundary: the TXT record is a pointer, and DNS without DNSSEC is spoofable. The HTTPS fetch that follows gives you transport security for whatever host you end up at — but if an attacker can forge the TXT record, they can point you at a host they control that has a perfectly valid certificate. If that matters for your threat model, that's exactly what discovery.signature is for, pinned to a key you obtained out of band.


How to contribute to ADS

The project has three doors, and CONTRIBUTING.md is blunt that picking the right one matters more than what you're proposing.

Three doors for contributing: editorial fixes ship as a normal PR and merge as a PATCH; spec changes start with an issue then a proposal document, never a direct PR to spec/; namespace registration is a one-line PR with no proposal cycle.

Door 1 — something is unclear, wrong, or typo'd

Open an issue, or just send a PR. This covers:

  • Broken links, typos, formatting
  • A section ambiguous enough that two implementers would read it differently
  • An example that doesn't actually validate against the schema

No process overhead. Purely editorial changes (nothing a conformant implementation would do differently) merge as a PATCH.

There's a dedicated spec-bug issue template with a nice prompt: "If two implementers would read this differently, describe both readings." That framing turns a vague complaint into an actionable report.

Door 2 — you want to change or extend the spec

This does not start with a PR to spec/. From CONTRIBUTING.md:

A drive-by PR that changes the manifest schema is the same failure mode as a drive-by PR to a wire protocol — it might be a great idea, but it needs to be proposed, discussed, and given a chance for implementers to object before it lands, not after.

The flow:

  1. Open an issue describing the problem — not the solution yet. Use the proposal-discussion template. It asks what you've already tried within the current spec, which catches the common case where the answer is "use the extensions block" or "register a prefix" rather than a Core change.
  2. If there's appetite, write a proposal from proposals/TEMPLATE.md, filed as proposals/0000-short-slug.md. An editor assigns the real number on merge.
  3. It moves Draft → Review → Last Call → Final. Only a Final proposal gets folded into spec/ at the next version bump.

The ADS proposal lifecycle: Draft to Review to Last Call to Final, with Withdrawn as a terminal state and Stagnant as a revivable one.

Proposal types:

Type What it changes
Core The manifest schema, discovery flow, versioning rules — anything in spec/
Extension A new optional, namespaced block. Documented alongside Core, never folded into it
Meta Process: the proposal process itself, governance, contributing
Informational Non-normative guidance. Carries zero conformance weight

A proposal must contain, before it can reach Review:

  • Summary — one paragraph, plain language
  • Motivation — a concrete manifest or scenario the current spec can't express. "This would be nice" is explicitly not motivation
  • Specification — the actual normative text or schema diff, written the way you'd want it to read inside spec/
  • Rationale — alternatives considered, and why this shape won. If you looked at how OpenAPI/MCP/OIDC solved something similar and diverged, say so
  • Backward compatibility — the required bump level and what existing manifests/clients need to do. "No impact" must be stated, not omitted
  • Security considerations"None" is acceptable; skipping the section is not, because an omitted section reads as "not considered"
  • Reference implementation — optional but strongly encouraged. Proposals with a working reference move through Review measurably faster

Door 3 — register a namespace prefix

Smallest door. Add one entry to registry/namespaces.json, alphabetically sorted:

{
  "prefix": "acme",
  "owner": "Acme Corp",
  "contact": "https://acme.example",
  "registeredDate": "2026-07-27"
}

An editor checks three things: the prefix isn't taken, it isn't core (reserved) or confusingly similar to an existing entry, and the contact is real and reachable. No proposal cycle. Releasing a prefix later is a PR removing your entry, with no cooldown.

Style rules for spec documents

If you're writing normative text, four rules from CONTRIBUTING.md:

  1. Use MUST / MUST NOT / SHOULD / MAY in the RFC 2119 sense, and say so the first time a document uses them.
  2. Prefer a worked example over an abstract description. "If a rule can't be shown in a 10-line JSON snippet, the rule is probably too complicated."
  3. Non-normative commentary goes in a marked > **Rationale:** blockquote, never mixed into normative text.
  4. Every schema change needs a matching example under spec/schema/examples/ that actually validates. Don't hand-wave it.

That second rule is a genuinely good constraint, and the kind of thing more specs should adopt — it forces complexity to show itself early.

PR checklists

The PR template has four sections; delete the ones that don't apply. For folding a Final proposal into spec/, the checklist is the one to internalise:

  • References the ADS number(s) being folded in
  • spec/schema/manifest.schema.json updated to match
  • spec/schema/examples/ updated/added and re-validated
  • CHANGELOG.md entry added
  • Version bump level matches the versioning strategy

Security reports

Spec-level security issues — discovery flow spoofing, cache poisoning, downgrade attacks, an extension block that a conformant client could misread as a Core field — go through a private GitHub Security Advisory, not a public issue. See SECURITY.md.

Bugs in a specific implementation go to that product's own security contact — unless the root cause is that the spec required or encouraged the unsafe behaviour, in which case both reports are useful.


Open questions and good first issues

The spec is at 0.1.0 with two commits and one proposal (ADS-0, the process itself). That's a genuinely good time to contribute — the surface is small enough to hold in your head, and nothing has ossified. Here's what I'd look at, roughly in order of "most likely to bite a real implementer."

1. Signature canonicalisation is unspecified. discovery.signature says to sign the document with the signature field removed, but never says how to serialise it. Two conformant servers can produce different bytes for identical content. Referencing RFC 8785 (JCS) would close this. This is a MINOR-level Core proposal with a clear motivation section already written for you.

2. The namespacedName regex rejects real domains. The pattern is:

^[A-Za-z][A-Za-z0-9-]{0,63}(\.[A-Za-z][A-Za-z0-9-]{0,63})+$

Every segment must start with a letter and may contain only letters, digits, and hyphens. Checked against the shipped schema:

PASS  com.example.lookupOrder
PASS  cheela.runtime
FAIL  com.7-eleven.checkStock      ← segment starts with a digit
FAIL  com.example.foo_bar          ← underscore

Reverse-DNS for a domain like 7-eleven.com gives com.7-eleven.checkStock, and that doesn't validate. Since the whole namespacing story is "your domain is yours by construction," a domain that can't be expressed is a real bug — and label-leading digits are perfectly legal in DNS. Per the versioning table, loosening a regex that rejected intended-valid manifests is a PATCH. Easy first contribution.

3. Nothing enforces capability uniqueness. The schema allows two entries in capabilities[] with the same name and the same version pointing at different endpoints. Multiple versions of one capability side-by-side is clearly intentional and useful; same name and same version is ambiguous, and "select by name + version range" has no defined tiebreaker. Worth either a schema constraint or an explicit "first match wins" sentence.

4. Case-insensitive namespaces are prose-only. The schema can't express "compare the namespace portion case-insensitively," so two manifests differing only in namespace case both validate while being, per the prose, the same namespace. Clients need a normalisation rule, and right now each will invent its own. A short normative "clients MUST case-fold the namespace portion before comparison" would settle it.

5. The well-known URI isn't registered. RFC 8615 establishes an IANA registry for .well-known URI suffixes precisely to prevent collisions. agent-discovery.json isn't in it. Registration is a form, not a spec change — but it's the difference between a convention and a reserved name.

6. The schema $id doesn't resolve. manifest.schema.json declares $id: https://agent-discovery-spec.org/schema/0.1.0/manifest.schema.json. That hostname doesn't currently resolve. A $id is formally an identifier, not necessarily a fetchable locator, so this is legal — but tooling authors and IDE integrations routinely try to fetch it, and versioned schema hosting is cheap. Either serve it or add a note in the README that it's an identifier only.

7. There's no conformance test suite. GOVERNANCE.md notes that building one would itself be a proposal, not something editors add unilaterally. A corpus of should-pass and should-fail manifests — plus the behavioural cases the schema can't express, like "client skips unknown transport instead of failing" — would do more for interoperability than any amount of prose. This is the highest-leverage contribution available right now.

8. Discovery is not authorisation. Worth stating explicitly in the spec: a public manifest tells the world your internal topology. The full-featured example includes mcp://support-escalation.internal.example.com, which is a hostname you may not want to publish. Nothing is wrong there — but a short "operators SHOULD consider what a public manifest reveals" note would prevent a predictable class of mistake.

9. Version negotiation syntax needs one clarifying sentence. Accept: application/json; ads-version=^1.2 is valid as written (^ is a legal tchar), but ranges containing spaces or > would need quoting. One sentence saves implementers a round trip through RFC 9110.

None of these are indictments. They're the normal residue of a spec that's been written but not yet ground against many implementations — which is precisely the phase where outside eyes are worth the most.


FAQ

What is the Agent Discovery Specification? ADS is an open, MIT-licensed, provider-agnostic specification defining a JSON capability manifest for agent-addressable systems, plus the flow a client uses to discover and validate it — primarily at /.well-known/agent-discovery.json.

Where is an ADS manifest served? For any HTTP(S)-addressable system, the manifest MUST be available at https://{host}/.well-known/agent-discovery.json, served as application/json. DNS TXT records and local files are supported alternatives for contexts without an HTTP handshake.

Is ADS a replacement for MCP? No. They operate at different layers and are complementary. MCP defines a full protocol including invocation; ADS defines only pre-connection discovery, and a manifest can legitimately point at an MCP server via "transport": "mcp".

Does ADS define how to authenticate? No. A manifest declares which kind of auth a capability requires — none, apiKey, oauth2, mtls, or a custom:-prefixed value — and MAY include free-form hints in authDetails. Completing the flow is out of scope.

What happens if a client sees a transport it doesn't recognise? It skips that capability and continues. Failing the whole manifest is explicitly a conformance bug. The same applies to unknown auth values, unknown top-level fields, and unrecognised extensions entries.

Do I need to register a namespace to use ADS? No. If you control example.com, then com.example.* is yours by construction, with no registration. The short-prefix registry is an optional convenience for projects that want a memorable prefix.

Is ADS production-ready? It's at 0.1.0 with Draft status. Per the pre-1.0 note, MINOR bumps may include breaking changes while it stabilises — though every such change must be flagged in the changelog. Treat it as adoptable-with-pinning, not frozen.

Who controls the specification? No single implementer, including Cheela Labs, gets standing beyond any other implementer's. Editors handle process, not technical veto; disputes default to not shipping the change; and there is deliberately no BDFL tiebreaker.

How do I propose a change to ADS? Open an issue describing the problem first. If there's appetite, write a proposal from proposals/TEMPLATE.md and open it against proposals/. It moves through Draft → Review → Last Call → Final, and only Final proposals get folded into the spec. Direct PRs to spec/ are not the path for normative changes.


Where this goes next

ADS is small on purpose, and its best ideas are the ones a much larger spec would have gotten wrong: separating three version numbers instead of overloading one, making unknown-field tolerance a normative MUST rather than a nice-to-have, requiring auth so that "unauthenticated" is a declaration rather than an omission, and refusing to define invocation at all.

Whether it becomes the discovery layer or one of several is not a question the document can answer. The neighbourhood is getting crowded, and interoperability between discovery formats may end up mattering more than any one of them winning.

What is clear is that "what can this system do, and how do I start?" deserves a better answer than "depends whose SDK you imported." ADS is a serious attempt at one, and it's small enough right now that a careful reader can meaningfully improve it in an afternoon.

Start here:


agent discoveryAI agentscapability manifestagent interoperabilityMCPopen standardswell-known URIJSON Schema