DEV Community

Cover image for Implement agent discovery in 10 minutes
Viren Tanti
Viren Tanti

Posted on • Originally published at blogs.cheelalabs.com on

Implement agent discovery in 10 minutes

Implement agent discovery in 10 minutes

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
Enter fullscreen mode Exit fullscreen mode

Create .well-known/agent-discovery.json:

{
  "specVersion": "0.3.0",
  "id": "com.example.bookshop",
  "name": "Example Bookshop",
  "provider": { "name": "Example Inc." },
  "capabilities": []
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In a second terminal, fetch it the way a client would:

curl -i http://localhost:8000/.well-known/agent-discovery.json
Enter fullscreen mode Exit fullscreen mode

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 not https://? 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
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode
python3 validate.py
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode
python3 validate.py
Enter fullscreen mode Exit fullscreen mode

✅ 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})+$'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode
[
  {
    "name": "search_books",
    "description": "Search the catalogue by title or author.",
    "input_schema": {
      "type": "object",
      "properties": { "query": { "type": "string" } },
      "required": ["query"]
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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

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.

Top comments (0)