# Implement agent discovery in 10 minutes

> Source: <https://dev.to/virentanti/implement-agent-discovery-in-10-minutes-2cc3>
> Published: 2026-08-05 18:59:01+00:00

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](https://github.com/Cheela-Labs/agent-discovery-spec), 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 client`http://`

and not`https://`

?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`

:

``` python
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:

``` python
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

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

[spec-bug issue](https://github.com/Cheela-Labs/agent-discovery-spec/issues/new/choose).

A change to how it works is a

[proposal](https://github.com/Cheela-Labs/agent-discovery-spec/blob/main/proposals/0000-process.md).

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](https://cheelalabs.com) 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.
