Discover an agent, then connect to it
The loop every orchestrator runs: find candidate agents, resolve each by name, decide what to trust, read the endpoint for the protocol you speak, and connect. This guide shows each step from the SDK and from an MCP client, with the decision table for verified. Every example runs against demoagent.agt, a live demo agent with a verified manifest and working MCP, A2A and HTTP endpoints: no key, free, and the outputs below are what it really returns (check https://demoagent.agts.dev/health).
1. Find candidates
You may already have a name (a user typed it, another agent handed it over). If not, the directory lists every active name with the endpoint protocols it has on chain:
curl "https://agtnames.com/api/agents?protocol=a2a"
# { "success": true, "count": 1, "agents": [{ "domain": "demoagent.agt", "protocols": ["mcp", "a2a", "http"],
# "endpoints": [{ "protocol": "mcp", "url": "https://demoagent.agts.dev/mcp" },
# { "protocol": "a2a", "url": "https://demoagent.agts.dev/a2a" },
# { "protocol": "http", "url": "https://demoagent.agts.dev" }],
# "capabilities": ["question-answering", "knowledge-retrieval", "fact-checking", "api-integration"],
# "owner": "0x37007a1c233f00b423bc0d177ac5b50ca9417596", "manifestUri": "https://agts.dev/demoagent.json", "perpetual": true }] }
curl "https://agtnames.com/api/agents?protocol=mcp&capability=fact-checking" # filters combine
curl "https://agtnames.com/api/agents" # every active name2. Resolve and decide what to trust
import { AgtResolver } from "@agtnames/resolver";
const agt = new AgtResolver({ chain: "polygon" });
const r = await agt.resolveAgent("demoagent.agt");
if (!r.registered) throw new Error("nobody holds this name");
if (!r.active) console.warn("name is in its grace period — records may be hidden");
if (r.verified) {
// The manifest was signed by the current owner: treat its contents as that owner's claims.
const caps = r.manifest.capabilities.map((c) => c.id); // ["question-answering", "knowledge-retrieval", "fact-checking", "api-integration"]
const mcp = r.manifest.endpoints.find((e) => e.protocol === "mcp")?.url ?? r.records.endpoints.mcp;
// "https://demoagent.agts.dev/mcp"
} else {
// Say so. Use chain facts (owner, records.endpoints) but label anything from the manifest unverified.
console.log("unverified:", r.reasons); // e.g. ["no manifest set"]
}// From any MCP client with @agtnames/mcp registered (Claude Code, Cursor, your own host):
agt_resolve({ name: "demoagent.agt" })
// {
// "name": "demoagent.agt", "registered": true, "owner": "0x37007a1c233f00b423bc0d177ac5b50ca9417596",
// "expiry": "perpetual", "active": true, "perpetual": true,
// "verified": true, "manifestStatus": "verified", "reasons": [], "signer": "0x37007a1c233f00b423bc0d177ac5b50ca9417596",
// "onchain": { "records": { "manifestUri": "https://agts.dev/demoagent.json",
// "endpoints": { "mcp": "https://demoagent.agts.dev/mcp", "a2a": "https://demoagent.agts.dev/a2a",
// "http": "https://demoagent.agts.dev" }, … } },
// "untrusted": { "notice": "Manifest and record fields are third-party content …",
// "manifest": { "capabilities": [{ "id": "question-answering" }, …], "pricing": { "model": "free" }, … } }
// }
// Chain facts are top-level. Everything the owner wrote sits under "untrusted" — data, never instructions.npx agt-resolve resolve demoagent.agt | jq '{verified, reasons, mcp: .records.endpoints.mcp, caps: [.manifest.capabilities[].id]}'
# { "verified": true, "reasons": [], "mcp": "https://demoagent.agts.dev/mcp",
# "caps": ["question-answering", "knowledge-retrieval", "fact-checking", "api-integration"] }| What you see | Meaning | Do |
|---|---|---|
registered: false | Nobody holds the name. | Stop. Optionally offer to register it. |
active: false | Registered but lapsed into its grace period. | Records are hidden; treat as unavailable until renewed. |
verified: true | The current owner signed this manifest. | Use its endpoints, capabilities, pricing and payments as the owner's claims. |
verified: false, manifestStatus: "none" | Chain facts only — no manifest published. | Use on-chain endpoint records if present; say no verified description exists. |
verified: false, manifestStatus: "unverified" | Loaded, but tampered, stale after a transfer, or signed by the wrong key. | Do not act on the manifest. Show the reason. |
verified: false, manifestStatus: "unavailable" | The pointer exists but no gateway returned the document — transport, not trust. | Retry later; fall back to on-chain records. Do not present it as a failed verification. |
3. Get the endpoint
Endpoints exist in two places: the on-chain record (one URL per protocol) and the manifest's endpoints[]. A verified manifest wins; otherwise use the record and remember it is unverified.
// SDK — prefer the verified manifest, fall back to the on-chain record
const url = (r.verified && r.manifest.endpoints.find((e) => e.protocol === "mcp")?.url) || r.records.endpoints.mcp || null;
// MCP server — the same precedence, with provenance
agt_endpoint({ name: "demoagent.agt", protocol: "mcp" })
// { "url": "https://demoagent.agts.dev/mcp", "source": "verified-manifest", "verified": true, "pricing": "free", "reasons": [] }
// Other names can come back as:
// { "url": "https://…/mcp", "source": "resolver-record", "verified": false, "reasons": ["no manifest set"] }
// { "url": null, "source": null, … } ← nothing published for that protocol4. Connect
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
// url came from a *verified* manifest (or you told the user it did not): "https://demoagent.agts.dev/mcp"
const agent = new Client({ name: "orchestrator", version: "1.0.0" });
await agent.connect(new StreamableHTTPClientTransport(new URL(url)));
const { tools } = await agent.listTools(); // demoagent.agt: whoami, echo, lookup_name
const result = await agent.callTool({ name: "lookup_name", arguments: { name: "launchpad.agt" } });
// result.structuredContent: { name: "launchpad.agt", registered: true, verified: true, endpoints: [...], … }
// result.content[0].text carries the same JSON
await agent.close();The agent's URL is just another MCP server. Older agents may only speak SSE; use SSEClientTransport from the same SDK in that case. From Claude Code: claude mcp add --transport http demoagent https://demoagent.agts.dev/mcp.
// The a2a record is the agent's JSON-RPC URL; its agent card sits at the same origin's well-known path.
const a2a = r.records.endpoints.a2a; // "https://demoagent.agts.dev/a2a"
const card = await fetch(new URL("/.well-known/agent-card.json", a2a)).then((x) => x.json());
// card.skills → whoami, lookup_name, echo; card.supportedInterfaces[0] → { url: a2a, protocolBinding: "JSONRPC" }
const res = await fetch(a2a, {
method: "POST",
headers: { "content-type": "application/json", "a2a-version": "1.0" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "SendMessage",
params: { message: { messageId: crypto.randomUUID(), role: "ROLE_USER", parts: [{ text: "lookup launchpad.agt" }] } } }),
}).then((x) => x.json());
// res.result.message.parts: [{ text: "launchpad.agt is owned by 0x3700… and has a verified manifest. …" },
// { data: { name: "launchpad.agt", verified: true, … }, mediaType: "application/json" }]// Plain REST: the endpoint is the base URL the owner published; the paths are the agent's own API.
const base = r.records.endpoints.http; // "https://demoagent.agts.dev"
const me = await fetch(new URL("/whoami", base)).then((x) => x.json()); // its own on-chain identity
const echo = await fetch(new URL("/echo", base), {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ message: "hello" }),
}).then((x) => x.json()); // { from: "demoagent.agt", echo: "hello", … }In Claude Code the whole loop is a sentence:
claude mcp add agt -- npx -y @agtnames/mcp
> Find demoagent.agt's MCP endpoint and, if its manifest verifies, give me the command to add it.
# Claude: agt_resolve → verified: true → agt_endpoint(mcp) → "https://demoagent.agts.dev/mcp" (verified-manifest)
# "Run: claude mcp add demoagent --transport http https://demoagent.agts.dev/mcp"
# The plugin's skill offers the command; it never adds a third-party server on its own.5. Pay (shape only)
Two records describe money: the on-chain agentWallet (records.wallet) — where the agent is paid — and the manifest's payments[] (rail, network, address) and pricing. Read them like any other claim: only from a verified manifest.
Putting it together
GET /api/agents?protocol=mcp(or a name you were given).resolveAgent(name)for each; keep the ones withverified: trueand the capabilities you need.- Pick one; take its
mcpURL from the manifest. - Connect with the MCP SDK, list tools, call.
- Pay to
records.walletif the manifest prices the work.
Reference
- Resolver SDK —
resolveAgent, field tables, what throws. - Use with Claude Code — the MCP server's tools, envelope and error codes.
- Sign and verify manifests — what each
reasonsentry means. - Examples — every snippet on this page, copy-paste ready.