Your First Agent Card
An Agent Card is a JSON document that a Beach application publishes at /.well-known/agent-card.json. It tells other agents, and the tooling around them, who you are, which protocols you speak, what capabilities you offer, and which Beach extensions you use. It is the discovery contract.
This is the five-minute introduction. For the full schema and the Beach-specific extensions, see Reference: agent-card.
The minimum
import { buildAgentCard } from '@cool-ai/beach-protocol';
const agentCard = buildAgentCard({
name: 'travel-concierge',
version: '1.0.0',
description: 'A travel-planning assistant that suggests destinations and books quotes.',
transports: [
{ protocol: 'a2a', endpoint: 'https://travel.example.com/a2a' },
],
capabilities: [
{
id: 'suggest-destination',
description: 'Suggest a holiday destination based on a free-text brief.',
inputs: [{ partType: 'user-message' }],
outputs: [{ partType: 'response' }, { partType: 'domain-data' }],
approval: 'none',
},
{
id: 'create-quote',
description: 'Create a holiday quote for a chosen destination.',
inputs: [{ partType: 'user-message' }],
outputs: [{ partType: 'response' }, { partType: 'domain-data' }],
approval: 'required', // a user or operator must approve before booking
},
],
extensions: {
beach: {
version: '1.0.0',
envelope: {
produces: ['response', 'domain-data', 'a2ui-surface'],
consumes: ['user-message', 'peer-message'],
},
turnStates: ['awaiting', 'complete', 'clarifying', 'suspended'],
},
},
});
That is a complete Agent Card. Five fields are mandatory — name, version, description, transports, capabilities. The extensions.beach block declares Beach-specific behaviour for peer Beach agents that understand it; non-Beach peers will ignore the block.
Publishing the card
@cool-ai/beach-protocol ships an Express middleware that serves the card at the canonical path:
import express from 'express';
import { agentCardMiddleware } from '@cool-ai/beach-protocol';
const app = express();
app.use(agentCardMiddleware(agentCard)); // exposes GET /.well-known/agent-card.json
app.listen(443);
A peer agent fetches the URL, parses the JSON, and decides whether to call.
The five required fields
name and version
name is the application's identifier: short, machine-readable. version is independently semver'd. A consumer that pinned to ^1.0.0 follows minor bumps automatically; a major bump warns them of a possible breaking change.
description
One sentence describing what the application does. This is what appears in discovery directories, card browsers, and developer-facing tooling.
transports
How peer agents reach the application. At a minimum, A2A:
transports: [
{ protocol: 'a2a', endpoint: 'https://your-app.example.com/a2a' },
],
If the application also exposes tools via MCP, list both:
transports: [
{ protocol: 'a2a', endpoint: 'https://your-app.example.com/a2a' },
{ protocol: 'mcp', endpoint: 'https://your-app.example.com/mcp' },
],
A peer chooses by the shape of the work — MCP for a tool call, A2A for a conversation. See MCP vs A2A.
capabilities
What peers can ask the application to do. Capabilities are declarative: they describe inputs and outputs, not internal implementation. Peers see them; tools — the private internals — stay hidden.
{
id: 'suggest-destination',
description: 'Suggest a holiday destination based on a free-text brief.',
inputs: [{ partType: 'user-message' }],
outputs: [{ partType: 'response' }, { partType: 'domain-data' }],
approval: 'none' | 'required' | 'conditional',
}
A peer reading the card learns: "I send user-message, I get back response and domain-data, the call requires no approval." The peer does not know which tools the application uses internally to produce the answer, and does not need to.
Some applications expose capabilities that drive an entire orchestrator turn — the agent reasons, calls tools, replies. Others expose constrained capabilities that map almost one-for-one to a single handler. Either shape is legitimate. The card describes the interface, not the implementation.
What not to do
Do not put internal tool names in the card. Tools are private. If a peer can call book-flight directly, the implementation has leaked. Expose create-quote as the capability and let the orchestrator decide whether book-flight is one of the tools it invokes.
Do not invent custom transports. The canonical set is a2a, mcp, and webhook. Inventing myCustomProtocol makes the application uncallable by anyone who does not happen to know the private protocol.
Do not lie about approval. If a capability triggers a side effect that a careful peer would want to gate — booking, payment, deployment — set approval: 'required'. Beach-aware peers will not auto-call capabilities so marked.
Do not declare turn-states the application cannot produce. If the application never emits 'suspended' (because there are no HITL approvals), do not list it. A peer prepares for what the card advertises.
Do not forget to bump version. A peer that cached the card yesterday and fetches it today reads the version to decide whether to invalidate. Bump on every deploy that changes the surface.
What peers do with the card
Beach's outbound A2A adapter caches Agent Cards by URL and consults them when calling out:
import { callAgent } from '@cool-ai/beach-transport';
const reply = await callAgent('travel-concierge', {
parts: [{ text: 'Suggest somewhere quiet for a long weekend in late September.' }],
});
The adapter looks up travel-concierge in the local agent registry, fetches the cached card, finds the A2A transport endpoint, and POSTs JSON-RPC message/send. The application receives the message through its A2A inbound adapter, runs the orchestrator, and returns the reply.
A peer that is not itself a Beach application performs the same dance via standard A2A. It ignores the extensions.beach block. The application degrades gracefully: the non-Beach peer interaction works, just without Beach-specific extensions.
Multi-channel cards
If the application accepts inbound traffic on more than one transport — A2A, MCP, and a webhook, say — list all of them:
transports: [
{ protocol: 'a2a', endpoint: 'https://app.example.com/a2a' },
{ protocol: 'mcp', endpoint: 'https://app.example.com/mcp' },
{ protocol: 'webhook', endpoint: 'https://app.example.com/webhooks/inbound' },
],
A peer picks the transport that fits the work in front of it. A peer can also pick by availability: if the A2A endpoint is down, a peer that knows the MCP endpoint can fall back.
Related
- Reference: agent-card — full schema, all fields, Beach extensions.
- MCP vs A2A — which transport to declare for which use case.
- Being consumed by other applications — A2A inbound wiring.
- Consuming other agents — A2A outbound, calling peers through their cards.