Cheela Labs
← ALL POSTS
7 MIN READViren Tanti

Implement agent discovery in 10 minutes

Serve a capability manifest at /.well-known/agent-discovery.json, validate it against the real ADS schema, and make it callable from an LLM tool API.

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.


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

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

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.

agent discoveryADScapability manifesttutorialquickstartwell-known URIJSON Schema