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,
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.
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.
WhyThe spec says a clienthttp://
and nothttps://
?MUST NOTfetch
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.
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.
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
❌ 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
^[a-zA-Z0-9_-]{1,64}$
. A dot is rejected outright. So a conformant ADS name
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
** endpoint.auth is required**, even when it is
"none"
. Delete the line and
❌ 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.
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):
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
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.
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
A change to how it works is a
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.