{"slug": "implement-agent-discovery-in-10-minutes", "title": "Implement agent discovery in 10 minutes", "summary": "Cheela Labs released version 0.3.1 of the Agent Discovery Specification, an open standard that lets agents advertise their capabilities via a simple JSON manifest served at a well-known path. The spec requires only five fields in the manifest and mandates HTTPS except for local development, and the project provides a minimal Python validation script using jsonschema to check manifests against the normative schema.", "body_md": "By the end of this page you will have served a valid ADS manifest, validated it\n\nagainst the normative schema, and made one capability callable from an LLM\n\ntool-calling API. Ten minutes, a text editor, and Python — which you almost\n\ncertainly already have.\n\nThis is a quickstart for the [Agent Discovery\nSpecification](https://github.com/Cheela-Labs/agent-discovery-spec), an open,\n\n**Current spec version: 0.3.0** (released as `v0.3.1`\n\n). If you have seen an ADS\n\nexample declaring `0.1.0`\n\n, it predates two releases. Copy from here instead.\n\nFive fields. That is the entire requirement.\n\n```\nmkdir -p ads-demo/.well-known && cd ads-demo\n```\n\nCreate `.well-known/agent-discovery.json`\n\n:\n\n```\n{\n  \"specVersion\": \"0.3.0\",\n  \"id\": \"com.example.bookshop\",\n  \"name\": \"Example Bookshop\",\n  \"provider\": { \"name\": \"Example Inc.\" },\n  \"capabilities\": []\n}\n```\n\nAn empty `capabilities`\n\narray is valid. \"I speak ADS and I currently expose\n\nnothing\" is a real, useful answer — it is how a client tells the difference\n\nbetween a system that has no capabilities and a system that has never heard of\n\ndiscovery.\n\nServe it:\n\n```\npython3 -m http.server 8000\n```\n\nIn a second terminal, fetch it the way a client would:\n\n```\ncurl -i http://localhost:8000/.well-known/agent-discovery.json\n```\n\nYou should see `Content-type: application/json`\n\nin the response headers. The spec\n\nrequires it, and Python's dev server gets it right for free.\n\nWhyThe spec says a client`http://`\n\nand not`https://`\n\n?MUST NOTfetch\n\na manifest over plaintext HTTP — a manifest names endpoints an agent will\n\nsubsequently call and the auth it will present, so over a rewritable channel it\n\nis a redirection primitive. The single exception is loopback (`localhost`\n\n,\n\n`127.0.0.0/8`\n\n,`[::1]`\n\n) during local development, where there is no network\n\nattacker. You are inside that exception. Everywhere else, use TLS.\n\n**You have just performed agent discovery.** A GET to a predictable path. That is\n\ngenuinely the whole mechanism.\n\nThis is where you find out whether your file is correct instead of hoping.\n\n```\npip install jsonschema requests\n```\n\nSave as `validate.py`\n\n:\n\n``` python\nimport json, requests\nfrom jsonschema import Draft202012Validator\n\nSCHEMA_URL = (\n    \"https://raw.githubusercontent.com/Cheela-Labs/\"\n    \"agent-discovery-spec/v0.3.1/spec/schema/manifest.schema.json\"\n)\n\nschema = requests.get(SCHEMA_URL).json()\nmanifest = json.load(open(\".well-known/agent-discovery.json\"))\n\nerrors = sorted(Draft202012Validator(schema).iter_errors(manifest),\n                key=lambda e: list(e.path))\n\nif not errors:\n    print(\"✅ Valid manifest\")\nelse:\n    for e in errors:\n        location = \" → \".join(str(p) for p in e.path) or \"(root)\"\n        print(f\"❌ {location}: {e.message}\")\npython3 validate.py\n```\n\nThat URL is pinned to a tag on purpose. Pointing a validator at `main`\n\nmeans the\n\nthing you validate against can change under you between two runs; pin the\n\nversion you are targeting and upgrade deliberately.\n\nThere is no official `ads-validate`\n\nCLI yet. Thirty lines of `jsonschema`\n\nis the\n\nwhole tool, which is roughly the point of keeping the schema small.\n\nAn empty manifest is legal but boring. A capability needs three things: `name`\n\n,\n\n`version`\n\n, `endpoint`\n\n.\n\nReplace the file:\n\n```\n{\n  \"specVersion\": \"0.3.0\",\n  \"id\": \"com.example.bookshop\",\n  \"name\": \"Example Bookshop\",\n  \"description\": \"Search the catalogue and check stock.\",\n  \"provider\": { \"name\": \"Example Inc.\", \"url\": \"https://example.com\" },\n  \"capabilities\": [\n    {\n      \"name\": \"com.example.searchBooks\",\n      \"invocationName\": \"search_books\",\n      \"version\": \"1.0.0\",\n      \"description\": \"Search the catalogue by title or author.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": { \"query\": { \"type\": \"string\" } },\n        \"required\": [\"query\"]\n      },\n      \"endpoint\": {\n        \"transport\": \"http\",\n        \"address\": \"https://api.example.com/v1/books/search\",\n        \"auth\": \"none\"\n      }\n    }\n  ],\n  \"discovery\": { \"cacheTtlSeconds\": 3600 }\n}\npython3 validate.py\n```\n\n`✅ Valid manifest`\n\n.\n\nThree fields there are worth understanding, because they are the ones people get\n\nwrong.\n\n** name must contain a dot.** It is a reverse-DNS identifier, and the schema\n\n```\n❌ capabilities → 0 → name: 'searchBooks' does not match\n   '^[A-Za-z][A-Za-z0-9-]{0,63}(\\.[A-Za-z][A-Za-z0-9-]{0,63})+$'\n```\n\nThe namespace is what stops your `searchBooks`\n\nand someone else's `searchBooks`\n\nfrom colliding the moment two manifests are merged into one agent's tool list.\n\n** invocationName must not contain a dot.** This is the newest part of the\n\n`^[a-zA-Z0-9_-]{1,64}$`\n\n. A dot is rejected outright. So a conformant ADS `name`\n\n`invocationName`\n\nis the identifier to use where `name`\n\ncannot be. It is\n\npresentation only — `name`\n\nremains the sole identity. If you omit it, a client\n\nthat needs a constrained identifier must derive one by **replacing dots with\nhyphens**, and is now forbidden from truncating to a subset of segments. That\n\n** endpoint.auth is required**, even when it is\n\n`\"none\"`\n\n. Delete the line and\n\n```\n❌ capabilities → 0 → endpoint: 'auth' is a required property\n```\n\nMaking \"no auth\" an explicit statement rather than an omission is deliberate — a\n\nmissing field is indistinguishable from a forgotten one, and an agent should\n\nnever have to guess whether it needs a credential.\n\n**Break each of those three on purpose and run the validator.** Ninety seconds,\n\nand you will remember the rules for good. Then put them back.\n\nHere is the payoff, and the reason `invocationName`\n\nexists. This turns a manifest\n\ninto a tool list an LLM API will actually accept:\n\n``` python\nimport json\n\nmanifest = json.load(open(\".well-known/agent-discovery.json\"))\n\ndef tool_name(cap):\n    # ADS-2: prefer invocationName; otherwise dots → hyphens. Never truncate.\n    return cap.get(\"invocationName\") or cap[\"name\"].replace(\".\", \"-\")\n\ntools = [\n    {\n        \"name\": tool_name(cap),\n        \"description\": cap.get(\"description\", \"\"),\n        \"input_schema\": cap.get(\"inputSchema\", {\"type\": \"object\"}),\n    }\n    for cap in manifest[\"capabilities\"]\n    if cap[\"endpoint\"][\"transport\"] == \"http\"      # skip what you can't speak\n    and not cap.get(\"deprecated\")\n]\n\nprint(json.dumps(tools, indent=2))\n[\n  {\n    \"name\": \"search_books\",\n    \"description\": \"Search the catalogue by title or author.\",\n    \"input_schema\": {\n      \"type\": \"object\",\n      \"properties\": { \"query\": { \"type\": \"string\" } },\n      \"required\": [\"query\"]\n    }\n  }\n]\n```\n\nThat array can be passed straight to a tool-calling API. You went from a URL to a\n\nusable tool list without knowing anything in advance about the system behind it.\n\nNote the `transport`\n\nfilter. The rule is **skip what you do not understand, never\nreject the whole manifest**. A client that raises on an unrecognised transport\n\nYou have a valid, current, tool-callable manifest. To put it in production:\n\nserve the same document at `/.well-known/agent-discovery.json`\n\nover TLS, send\n\n`Access-Control-Allow-Origin: *`\n\nif browser clients should see it, and keep\n\n`specVersion`\n\nhonest when you upgrade.\n\nThe most useful thing you can do next is disagree with something here.\n\nADS is a 0.x draft. It is small, young, MIT-licensed with no CLA, and governed\n\nthrough a public proposal process modelled on Ethereum's EIPs — which makes it an\n\nunusually good first standards contribution. `invocationName`\n\nexists because\n\nsomeone hit the dot problem and wrote it up. Ambiguity in the spec is a\n\n[spec-bug issue](https://github.com/Cheela-Labs/agent-discovery-spec/issues/new/choose).\n\nA change to how it works is a\n\n[proposal](https://github.com/Cheela-Labs/agent-discovery-spec/blob/main/proposals/0000-process.md).\n\nBoth doors are open, and the second one is less intimidating than it sounds.\n\nIf you serve a manifest anywhere public, open an issue and say so. A spec with\n\none implementer is a design document; the implementations are what make it a\n\nstandard.\n\n[Cheela](https://cheelalabs.com) is the first production implementer of ADS —\n\nits runtime registry publishes conformant manifests for every registered\n\nruntime. The spec does not depend on it, and nothing in this quickstart used it.", "url": "https://wpnews.pro/news/implement-agent-discovery-in-10-minutes", "canonical_source": "https://dev.to/virentanti/implement-agent-discovery-in-10-minutes-2cc3", "published_at": "2026-08-05 18:59:01+00:00", "updated_at": "2026-08-05 19:26:39.739556+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Cheela Labs", "Agent Discovery Specification", "Python", "jsonschema"], "alternates": {"html": "https://wpnews.pro/news/implement-agent-discovery-in-10-minutes", "markdown": "https://wpnews.pro/news/implement-agent-discovery-in-10-minutes.md", "text": "https://wpnews.pro/news/implement-agent-discovery-in-10-minutes.txt", "jsonld": "https://wpnews.pro/news/implement-agent-discovery-in-10-minutes.jsonld"}}