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.
Table of contents
- The problem ADS actually solves
- What is the Agent Discovery Specification?
- ADS vs MCP vs OpenAPI vs A2A
- Anatomy of a capability manifest
- The discovery flow
- Three version numbers, three questions
- Extensibility: the one load-bearing rule
- Backward compatibility and the golden rule
- Governance: why it's modelled on EIPs
- Using the spec
- How to contribute to ADS
- Open questions and good first issues
- FAQ
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.
| 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:
- ADS and MCP are complementary, not alternatives. A perfectly reasonable ADS
manifest has
"transport": "mcp"and anaddresspointing at an MCP server. ADS gets you to the door; MCP is what you speak once inside. - 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. - 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:
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
transportvalues:http,grpc,mcp,queue - Well-known
authvalues: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+jsonstructured suffix) - MUST validate against the normative JSON Schema
- SHOULD support
ETag/Cache-Control/If-None-Match - SHOULD answer
HEADfor 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
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.
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,authvalue, orextensionsentry 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.fooandcom.example.fooare 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: trueMUST be accompanied bydeprecatedSince— and the JSON Schema actually enforces this with a conditionalif/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
deprecatedSinceandremovalNotBeforeMUST be at least one full MAJOR of that capability. Deprecating in1.3.0means the earliest legal removal is2.x— never a later1.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
- Reusing
idfor a materially different system. - 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:
- The file served at exactly
/.well-known/agent-discovery.json— note the leading dot; some build tools strip dot-directories by default. Content-Type: application/json(nottext/plain, which some hosts default to for unknown extensions — though.jsonis normally fine).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,formatis an annotation by default — a validator without format assertion enabled will happily accept"lastUpdated": "yesterday"and"url": "not a url". The manifest schema usesformatonlastUpdated,provider.url, andsignature.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:
404returnsnull, 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.
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:
- 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
extensionsblock" or "register a prefix" rather than a Core change. - If there's appetite, write a proposal from
proposals/TEMPLATE.md, filed asproposals/0000-short-slug.md. An editor assigns the real number on merge. - It moves
Draft → Review → Last Call → Final. Only aFinalproposal gets folded intospec/at the next version bump.
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
Reviewmeasurably 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:
- Use MUST / MUST NOT / SHOULD / MAY in the RFC 2119 sense, and say so the first time a document uses them.
- 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."
- Non-normative commentary goes in a marked
> **Rationale:**blockquote, never mixed into normative text. - 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.jsonupdated to match -
spec/schema/examples/updated/added and re-validated -
CHANGELOG.mdentry 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:
- 📄 The spec — six documents, readable in order
- 🔧 The JSON Schema — the normative source for structure
- 🧪 The examples — minimal and full-featured, both validate
- 🤝 CONTRIBUTING.md — pick the right door
- 📬 The proposal process — ADS-0, the EIP-1 equivalent