By the end of this page you will have served a valid ADS manifest, validated it against the normative schema, and made one capability callable from an LLM tool-calling API. Ten minutes, a text editor, and Python — which you almost certainly already have.
This is a quickstart for the Agent Discovery Specification, an open, MIT-licensed, vendor-neutral spec. Nothing here requires a product, an account, or a signup. If you want the reasoning behind any of it, the two deep dives linked at the bottom are where it lives — but read them after this works.
Current spec version: 0.3.0 (released as v0.3.1). If you have seen an ADS
example declaring 0.1.0, it predates two releases. Copy from here instead.
Step 1 — serve the smallest legal manifest
Five fields. That is the entire requirement.
mkdir -p ads-demo/.well-known && cd ads-demo
Create .well-known/agent-discovery.json:
{
"specVersion": "0.3.0",
"id": "com.example.bookshop",
"name": "Example Bookshop",
"provider": { "name": "Example Inc." },
"capabilities": []
}
An empty capabilities array is valid. "I speak ADS and I currently expose
nothing" is a real, useful answer — it is how a client tells the difference
between a system that has no capabilities and a system that has never heard of
discovery.
Serve it:
python3 -m http.server 8000
In a second terminal, fetch it the way a client would:
curl -i http://localhost:8000/.well-known/agent-discovery.json
You should see Content-type: application/json in the response headers. The spec
requires it, and Python's dev server gets it right for free.
Why
http://and nothttps://? The spec says a client MUST NOT fetch a manifest over plaintext HTTP — a manifest names endpoints an agent will subsequently call and the auth it will present, so over a rewritable channel it is a redirection primitive. The single exception is loopback (localhost,127.0.0.0/8,[::1]) during local development, where there is no network attacker. You are inside that exception. Everywhere else, use TLS.
You have just performed agent discovery. A GET to a predictable path. That is genuinely the whole mechanism.
Step 2 — validate it against the real schema
This is where you find out whether your file is correct instead of hoping.
pip install jsonschema requests
Save as validate.py:
import json, requests
from jsonschema import Draft202012Validator
SCHEMA_URL = (
"https://raw.githubusercontent.com/Cheela-Labs/"
"agent-discovery-spec/v0.3.1/spec/schema/manifest.schema.json"
)
schema = requests.get(SCHEMA_URL).json()
manifest = json.load(open(".well-known/agent-discovery.json"))
errors = sorted(Draft202012Validator(schema).iter_errors(manifest),
key=lambda e: list(e.path))
if not errors:
print("✅ Valid manifest")
else:
for e in errors:
location = " → ".join(str(p) for p in e.path) or "(root)"
print(f"❌ {location}: {e.message}")
python3 validate.py
That URL is pinned to a tag on purpose. Pointing a validator at main means the
thing you validate against can change under you between two runs; pin the
version you are targeting and upgrade deliberately.
There is no official ads-validate CLI yet. Thirty lines of jsonschema is the
whole tool, which is roughly the point of keeping the schema small.
Step 3 — add a capability that does something
An empty manifest is legal but boring. A capability needs three things: name,
version, endpoint.
Replace the file:
{
"specVersion": "0.3.0",
"id": "com.example.bookshop",
"name": "Example Bookshop",
"description": "Search the catalogue and check stock.",
"provider": { "name": "Example Inc.", "url": "https://example.com" },
"capabilities": [
{
"name": "com.example.searchBooks",
"invocationName": "search_books",
"version": "1.0.0",
"description": "Search the catalogue by title or author.",
"inputSchema": {
"type": "object",
"properties": { "query": { "type": "string" } },
"required": ["query"]
},
"endpoint": {
"transport": "http",
"address": "https://api.example.com/v1/books/search",
"auth": "none"
}
}
],
"discovery": { "cacheTtlSeconds": 3600 }
}
python3 validate.py
✅ Valid manifest.
Three fields there are worth understanding, because they are the ones people get wrong.
name must contain a dot. It is a reverse-DNS identifier, and the schema
enforces it. Drop the namespace and you get:
❌ capabilities → 0 → name: 'searchBooks' does not match
'^[A-Za-z][A-Za-z0-9-]{0,63}(\.[A-Za-z][A-Za-z0-9-]{0,63})+$'
The namespace is what stops your searchBooks and someone else's searchBooks
from colliding the moment two manifests are merged into one agent's tool list.
invocationName must not contain a dot. This is the newest part of the
spec (ADS-2,
landed in 0.3.0) and it exists because of a genuine collision: OpenAI, Anthropic,
Google and every OpenAI-compatible endpoint constrain tool function names to
^[a-zA-Z0-9_-]{1,64}$. A dot is rejected outright. So a conformant ADS name
could never be passed to any of them.
invocationName is the identifier to use where name cannot be. It is
presentation only — name remains the sole identity. If you omit it, a client
that needs a constrained identifier must derive one by replacing dots with
hyphens, and is now forbidden from truncating to a subset of segments. That
prohibition is not pedantry: two capabilities in one manifest can share a leaf
segment, and a client that truncates maps both to the same tool name silently.
endpoint.auth is required, even when it is "none". Delete the line and
the validator says so:
❌ capabilities → 0 → endpoint: 'auth' is a required property
Making "no auth" an explicit statement rather than an omission is deliberate — a missing field is indistinguishable from a forgotten one, and an agent should never have to guess whether it needs a credential.
Break each of those three on purpose and run the validator. Ninety seconds, and you will remember the rules for good. Then put them back.
Step 4 — hand it to an LLM
Here is the payoff, and the reason invocationName exists. This turns a manifest
into a tool list an LLM API will actually accept:
import json
manifest = json.load(open(".well-known/agent-discovery.json"))
def tool_name(cap):
# ADS-2: prefer invocationName; otherwise dots → hyphens. Never truncate.
return cap.get("invocationName") or cap["name"].replace(".", "-")
tools = [
{
"name": tool_name(cap),
"description": cap.get("description", ""),
"input_schema": cap.get("inputSchema", {"type": "object"}),
}
for cap in manifest["capabilities"]
if cap["endpoint"]["transport"] == "http" # skip what you can't speak
and not cap.get("deprecated")
]
print(json.dumps(tools, indent=2))
[
{
"name": "search_books",
"description": "Search the catalogue by title or author.",
"input_schema": {
"type": "object",
"properties": { "query": { "type": "string" } },
"required": ["query"]
}
}
]
That array can be passed straight to a tool-calling API. You went from a URL to a usable tool list without knowing anything in advance about the system behind it.
Note the transport filter. The rule is skip what you do not understand, never
reject the whole manifest. A client that raises on an unrecognised transport
breaks the first time anyone adds a capability it does not support — and per the
spec, that is the client's bug, not the manifest's.
You are done
You have a valid, current, tool-callable manifest. To put it in production:
serve the same document at /.well-known/agent-discovery.json over TLS, send
Access-Control-Allow-Origin: * if browser clients should see it, and keep
specVersion honest when you upgrade.
Where to go next
- The reasoning — Agent Discovery Spec: a .well-known for AI agents is the deep dive: worked examples, the versioning model, how ADS differs from MCP and OpenAPI.
- The gentler route — How AI agents find out what other systems can do covers the same ground assuming less.
- The normative text — the spec itself, seven documents, all short.
Now change it
The most useful thing you can do next is disagree with something here.
ADS is a 0.x draft. It is small, young, MIT-licensed with no CLA, and governed
through a public proposal process modelled on Ethereum's EIPs — which makes it an
unusually good first standards contribution. invocationName exists because
someone hit the dot problem and wrote it up. Ambiguity in the spec is a
spec-bug issue.
A change to how it works is a
proposal.
Both doors are open, and the second one is less intimidating than it sounds.
If you serve a manifest anywhere public, open an issue and say so. A spec with one implementer is a design document; the implementations are what make it a standard.
Cheela is the first production implementer of ADS — its runtime registry publishes conformant manifests for every registered runtime. The spec does not depend on it, and nothing in this quickstart used it.