An agent that speaks MCP and an agent that speaks A2A can both be reached through the same .agt name. The name does not pick a transport. It lists the endpoints the agent exposes, one per protocol, and the client takes the one it understands. This article is about that endpoints[] field, how it relates to the endpoint records stored on-chain, and what a client does with both.
The shape #
Each entry has a protocol, a url and an optional version:
"endpoints": [
{ "protocol": "mcp", "url": "https://exampleagent.example/mcp", "version": "2025-11-05" },
{ "protocol": "a2a", "url": "https://exampleagent.example/.well-known/agent.json" },
{ "protocol": "http", "url": "https://exampleagent.example/api/v1" },
{ "protocol": "ws", "url": "wss://exampleagent.example/stream" },
{ "protocol": "grpc", "url": "https://grpc.exampleagent.example:443" }
]
The protocol value is one of five reference ids or a custom string:
mcp: Model Context Protocol. The URL is the server endpoint an MCP client connects to.a2a: Agent-to-Agent Protocol. The URL points at the agent card, conventionally/.well-known/agent.json. The manifest does not embed the card; it points to it.http: a REST or RPC API over HTTPS. The URL is the base of the API.ws: WebSocket. The URL should bewss://.grpc: gRPC. The URL is the host and port the channel dials.
Custom protocol ids are permitted. Lowercase, hyphenated, the same rule as capability ids. A reader that does not recognize the id ignores the entry and moves on.
One entry per protocol. The manifest editor enforces this, along with https:// for web URLs and wss:// for WebSocket. If you have two MCP servers, put the one you want strangers to use in the manifest and document the other elsewhere.
Here is the live example. launchpad.agt exposes a single HTTP endpoint today:
"endpoints": [
{ "url": "https://agtnames.com/api/v2/manifest", "protocol": "http" }
]
Two places an endpoint can live #
Endpoints exist in two layers, and it is worth being precise about which is which.
The manifest is the signed document. Its endpoints[] is covered by the owner's EIP-191 signature. A client that verifies the manifest knows the owner published those URLs.
The resolver contract holds agentEndpoint(protocol) records, one URL per protocol, keyed to the name's node. These are plain on-chain writes from the owner wallet. They are fast to read with a single eth_call, they need no fetch of a document, and they are what a wallet, an indexer or a DNS projection reads when it does not want to parse JSON. The records reference lists them next to addr, text, agentManifest, agentWallet and agentKey.
The editor writes both in one transaction. When you sign and publish, the manifest goes to its host and a multicall on the resolver sets the manifest pointer and one setAgentEndpoint per protocol. Removing a protocol in the editor clears the on-chain record in the same call. So for a name published through the site, the two layers agree.
They can drift when someone writes one layer from code and forgets the other. That is fine, as long as readers know which one wins.
Which one wins #
The agt_endpoint tool in the @agtnames/mcp server answers exactly this question. Ask it for a name and a protocol:
{ "name": "exampleagent.agt", "protocol": "mcp" }
It resolves the name, then applies a two-step rule. If the manifest verifies, it takes the URL from the manifest's endpoints[]. Otherwise it falls back to the on-chain agentEndpoint record for that protocol. The response says which one it used:
{
"name": "exampleagent.agt",
"protocol": "mcp",
"url": "https://exampleagent.example/mcp",
"source": "verified-manifest",
"verified": true,
"pricing": "freemium",
"reasons": [],
"notice": "..."
}
source is verified-manifest or resolver-record. verified is true only when the URL came from a manifest that passed the three-way check (signer equals manifest.owner equals on-chain owner). A URL from the resolver record is still published by the owner, since only the owner wallet can write it, but the tool marks it verified: false so that an agent treats it as unverified third-party data rather than a signed statement. pricing comes from the verified manifest only and is null otherwise.
The tool's protocol input accepts mcp, a2a, http and ws. For grpc or a custom protocol, read the full manifest with agt_manifest or the SDK and pick the entry yourself.
The @agtnames/resolver package exposes both layers side by side, so you can apply the same rule or your own:
import { AgtResolver } from "@agtnames/resolver";
const agt = new AgtResolver({ chain: "polygon" });
const r = await agt.resolveAgent("exampleagent.agt");
r.records.endpoints.mcp; // on-chain agentEndpoint("mcp")
r.verified && r.manifest?.endpoints; // the signed list, only when verified
The rule of thumb: prefer the signed list when it verifies, use the on-chain record when it does not, and tell the caller which one you used.
Dispatching on the protocol you speak #
A client does not iterate endpoints[] and try each one. It looks for the protocol it implements and connects to that URL. Everything else is ignored.
An MCP client, for instance a coding assistant with the agt server installed, asks agt_endpoint for mcp and registers the returned URL as a remote MCP server. It never looks at the A2A card.
An A2A client asks for a2a, fetches the agent card at that URL, and proceeds with the A2A handshake. It never opens an MCP session.
A plain script that wants a REST call asks for http, appends its path to the base URL, and sends the request. If pricing came back as paid or freemium, it knows to look at the manifest's payments[] before calling.
A streaming consumer asks for ws and opens the socket. A service mesh asks for grpc and dials the channel.
The point is that the manifest is protocol-neutral and the dispatch is protocol-specific. The agent publishes once. Each client does one lookup and one connect. Adding a sixth protocol later is one more entry in the list and one more resolver record, and clients that do not speak it are unaffected.
Examples per protocol #
Hypothetical manifests, one per protocol, showing what a well-formed entry looks like:
{ "protocol": "mcp", "url": "https://research.example/mcp", "version": "2025-11-05" }
The version here is the MCP protocol revision the server implements. An MCP client can check compatibility before connecting.
{ "protocol": "a2a", "url": "https://research.example/.well-known/agent.json" }
The agent card carries its own version and skills. The manifest only needs to say where it is.
{ "protocol": "http", "url": "https://research.example/api/v2", "version": "2" }
A base URL and the API major version. Paths and methods belong in your API docs, or in the input and output schemas on a capability.
{ "protocol": "ws", "url": "wss://research.example/stream" }
Secure WebSocket. The editor rejects ws://.
{ "protocol": "grpc", "url": "https://grpc.research.example:443", "version": "v1" }
Host, port and the service version. The .proto contract is published wherever you publish it; the manifest points at the channel.
{ "protocol": "acp", "url": "https://research.example/acp" }
A custom protocol id. Readers that know acp will use it. Readers that do not will skip it.
Versioning advice #
version is optional and free-form, so use it for the thing a client would check before connecting. For mcp that is the protocol revision date. For http and grpc it is your API's major version. For a2a leave it out; the card carries its own.
When you ship a breaking change, publish a new manifest with the new version. Since the manifest carries an updated timestamp and the whole body is signed, a client that cached the old one can see the difference and re-verify. If the manifest is hosted at https://agts.dev/<label>.json, the republish keeps the same URL and costs nothing on-chain. Update the on-chain agentEndpoint record too if the URL changed, so the fallback path stays correct.
Run old and new side by side during a migration if you can, and change version only when the URL or the contract actually changed. A client that compares version should never see a change that means nothing.
Where this leaves you #
A .agt name is a single lookup that returns every transport your agent supports, signed by the wallet that owns the name. Publish the protocols you actually serve, keep the on-chain records in step, and let each client pick its own.
The records reference documents the on-chain agentEndpoint record alongside the rest of the resolver model, with the contract address and the write calls. Start there if you are publishing from code, or open the manifest editor if you would rather sign in your wallet.