Tutorial

How does an AI agent find another agent? Agent Registry, A2A Agent Card, and JSON capability discovery

When discovery fails, name the hop first: catalog search, card fetch, or a domain you already hold with the wrong GET.

The last post, the Agent Card field tutorial, opened the 1.0 required table. Today we do not refill fields, and we do not retell why a Registry is a catalog. The orchestrator’s next question is: how do I get from “I need a colleague who classifies returns” to a callable Card? The official answer is on A2A Agent Discovery: three strategies, one Card. The spec does not define a curated-registry query API — see Considerations on that page. Google Cloud’s catalog is one implementation; shape it against the Registry JSON schemas. This post walks hops. Sideways delegation versus calling down into tools lives in A2A vs MCP; we do not repeat that split.

Discovery is not a protocol. The Card is what you find

A2A standardizes a self-description, not a phone book. A remote agent writes its abilities as a JSON card. A client uses that card to decide fit, how to connect, and what Task to send. The method changes with the environment: public internet, an enterprise catalog, a hardcoded URL on a laptop. All three can land on the same Card. Call “discovery” another conversation protocol and the review walks off the path. The protocol is still A2A. What changes is which door you use to reach well-known or a catalog.

The official discovery page still talks about a top-level url in “Role of the Agent Card” — that is 0.3 language. New cards follow 1.0: the endpoint sits in supportedInterfaces. See what’s new in v1.0. If a hop still reads the old field, do not treat it as the 1.0 contract. How to write the fields is the previous post. Today we only ask: which hop put this card in your hand, and which keys you check first.

A successful find is not a license to tools/call. The other side is still an opaque agent. You pick a skill, pick an interface, and send a Task. Treat a search hit as a function table and multi-turn clarification will blow through arguments. How an MCP hop checks parameters is the in-site Schema post. Today we stop at “who did we find, and why do we believe they handle this.”

Strategy What you already know Next hop
Well-known URIA domain or hostGET /.well-known/agent-card.json
Curated catalogSkill keywords / tagsQuery the catalog, then fetch the Card or a pointer
Direct configA URL or the whole cardSkip search; read the card

Three official strategies: pick a road, then walk it

Well-known fits a public agent, or discovery when you already control the hostname. The path follows RFC 8615: https://{agent-server-domain}/.well-known/agent-card.json. The client knows or can derive the domain, issues an HTTP GET, and receives JSON. The implementation is simple and easy to automate. If the card holds sensitive skills or an internal URL, that GET itself must be authenticated. Do not hang an intranet endpoint on the open internet.

A curated catalog fits an enterprise or a marketplace: a middle service collects cards, clients query by skills, tags, provider, or capabilities, and the catalog returns matching cards or pointers. You gain governance and search-by-ability. You also have to run the catalog. A2A does not specify that API. Google Agent Registry, a community registry, and a home-grown catalog each invent their own query shape. Do not validate three vendors against one “universal discover JSON.”

Direct configuration fits a tight couple, a private agent, or a laptop. The card URL lives in an env var, a config file, or a proprietary API. When the relationship is static, this is the cheapest path. When the Card moves, every client must move with it. In production, treating “hardcoded on the laptop” as the only discovery surface is a 2025 address book. The three roads can coexist: the catalog finds who, well-known still reads the card if the catalog is down, and config seeds a couple of stable hosts at boot.

You have a hostname: GET well-known

The cleanest hop is three steps. (1) Obtain a domain, for example returns.agents.example.com. (2) GET https://returns.agents.example.com/.well-known/agent-card.json. (3) The response is a Card that passes the 1.0 Schema. A wrong path, an internal alias instead of HTTP, or a certificate hostname mismatch will show up as “discovery failed.” The Card may be fine. The GET never landed.

The spec asks the Card endpoint to send cache headers. Cache-Control: max-age=… keeps intermediates and clients from fetching the full card every time. ETag can be the version or a content hash. After expiry, send a conditional request (If-None-Match) instead of an unconditional GET. If the server sends no cache headers, the client may pick a short default — but it must not cache forever a card whose skills can change.

A public card only needs enough for an orchestrator to choose. Sensitive skills and a second internal endpoint belong on an authenticated extended card. Fetch the second copy only when capabilities.extendedAgentCard is true. A discovery hop must not assume an extended card exists before auth. A catalog that returns different cards by identity, and a well-known card that is the same for everyone, are two disclosure models. Write them as two sentences in the review.

You have a catalog: search tags, then fetch the Card

When you have no hostname, only the sentence “find someone who classifies returns,” you walk a curated catalog. The query eats skills[].tags and skill names on the Card — not the topology on your slides. In-project Google orchestrators, Gemini Enterprise, and Agent Gateway search registered entries by skill keywords. That is product behavior, not an A2A standard RPC. Community and other clouds each have their own search. A review fixture should assert “query → hit list → each row has a cardUrl or an embedded Card.” Do not promote one vendor path to the spec.

When the catalog returns a pointer, the next hop is still well-known or the card URL the catalog gave you. When it returns an embedded Card, you still validate it against the 1.0 Schema: a successful registration is not a legal field set. A NO_SPEC entry that has a host and no skills has an empty search surface — #5 already said this. Today’s reminder: a keyword hop on an empty index is not a broken protocol.

A down catalog is not the end of discovery by itself. If you already hold a hostname, the client should still GET well-known. Treating the catalog as the only source of truth single-points the discovery surface. Keep one or two stable hosts in seed config, then resume keyword search when the catalog is back. That is closer to the spec’s three coexisting strategies than “catalog 500, we stop.”

After a hit: match skills, pick an interface, send a Task

A search hit only means “maybe this one.” The orchestrator still walks skills[]: is id the kind of work you want to delegate, do tags actually match the query, and do you accept the boundary in description? Do not wrap a skill in an MCP inputSchema. 1.0’s hints are examples and MIME types. Pick the wrong skill and the Task fails at delegation, not at discovery — but the fixture must write “hit ≠ selected” as two steps.

After you select, read supportedInterfaces. The first entry is preferred. The client picks a binding it can speak: JSONRPC, GRPC, or HTTP+JSON. No shared binding means discovery succeeded and the call failed. Do not treat a 0.3 top-level url as the 1.0 primary endpoint. Read the capability flags too: if streaming is false and you still subscribe to a stream, the spec wants a capability error, not a silent fallback to unary.

The JSON below is a walk fixture, not any vendor’s official catalog API. It folds query, hit, and preferred interface into one object so you can diff it next to the Card in the repo. The next verb is message/send. Validation checklists and signatures stay for a later post.

{
  "kind": "discovery-trace",
  "note": "CI/review fixture — not an official A2A or Google Registry API",
  "query": {
    "tags": ["returns", "classify"]
  },
  "strategy": "curated-registry",
  "hits": [
    {
      "name": "Returns Specialist",
      "cardUrl": "https://returns.agents.example.com/.well-known/agent-card.json",
      "matchedTags": ["returns", "classify"],
      "skillId": "classify-return"
    }
  ],
  "selected": {
    "skillId": "classify-return",
    "preferredInterface": {
      "url": "https://agents.example.com/returns/a2a",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    },
    "next": "message/send"
  }
}
This hop Input What to assert
Search the catalogTags or a skill nameHit list is non-empty and carries a pointer
Fetch well-knownA hostname or cardUrlJSON parses; it passes the 1.0 Schema
Select / pick an interfaceA complete 1.0 CardSkill id matches; the client speaks the interface

Cache, stale indexes, and fixtures

Cards do not move often: you bump them when you add a skill or change auth. The discovery surface still goes stale. The catalog index lags, well-known already serves a new version, and search still points at old tags. Keep at least two fixtures: a catalog-hit snapshot, and the Card you just GET. When keywords miss, check tags and index lag before you blame a 1.0 client that read a 0.3 card.

{
  "hop": "well-known",
  "method": "GET",
  "url": "https://returns.agents.example.com/.well-known/agent-card.json",
  "requestHeaders": {
    "If-None-Match": "1.0.3"
  },
  "response": {
    "status": 304,
    "etag": "1.0.3",
    "cacheControl": "max-age=3600"
  }
}

The spec is blunt: sensitive data requires auth. Prefer out-of-band dynamic credentials. Do not write a static secret into the Card. A discovery fixture that contains a token or an intranet password fails the review on sight. Write the public card as if it will be fetched. Cache for an extended card follows the session. Do not pour it into the same bucket as the public card’s max-age.

Do not treat a Plugin SKILL.md or MCP tools/list as a discovery source either. How a coding agent in the same repo grows skills is the box. How another team’s returns agent is found is the Card. Smash both into one capability.json and three failed hops land on the same ticket line.

You can finish this in the browser: JSON format to see whether the walk fixture and the Card parse; JSON Schema validate to check the 1.0 Card after a hit; JSON Diff to compare the catalog snapshot with the Card you just fetched and catch tags / interface drift. Data stays on this machine. Further reading: the Agent Card field tutorial, and the Registry overview. Signatures and the validation checklist are the next practical post.

Related: A2A Agent Card JSON Schema, Google Agent Registry, A2A vs MCP.

FAQ

If I have a catalog, do I still GET well-known?

Yes. The catalog consumes Cards; it does not replace them. The spec writes well-known as the standard path for public discovery. If the catalog is down, a client that still holds a hostname should read the card.

Does A2A define a standard “search agents” RPC?

No. The discovery page is explicit: the current spec does not prescribe a curated-registry API. Each catalog defines its own query. What is standard is the Card’s shape and the well-known path.

If I found them, can I tools/call?

No. A hit is a discovery result. The other side is an opaque agent; you send a Task. Deterministic parameters stay with MCP tools. Do not write them into the discovery hop.

How long should I cache?

Honor the server’s Cache-Control and ETag first. With no headers, use a short default and conditional requests after expiry. When skills or auth change, version should move. A client must not route production traffic on stale tags.

Takeaways and next steps

In 2026, “automatically find another agent” collapses to three hops: pick a strategy, obtain a Card, then decide whether to delegate from skills and interfaces. A catalog is one door, not the protocol.

Ship in this order: hang well-known correctly for a public agent; if you need keyword search, plug in one vendor catalog and bring your own query fixture; after a hit, validate the 1.0 Card, then send a Task. How to write the fields is the previous post. What a catalog is is #5. Validation and signatures come later. Diff the walk JSON against the repo Card in JSONVue.