Skip to main content

A2A (Agent-to-Agent)

Mastra supports version 0.3.0 of the Agent-to-Agent (A2A) protocol for cross-platform multi-agent systems. Use A2A to expose Mastra agents as remote agents or consume remote A2A agents as Mastra subagents. You can also call A2A endpoints with the JavaScript client SDK.

A2A is an open protocol for delegating work to agents across network, framework, vendor, and language boundaries. A remote agent keeps its own tools, prompts, memory, workflows, and infrastructure private while exposing a protocol endpoint that other systems can discover and call.

When to use A2A
Direct link to When to use A2A

  • A parent agent should delegate work to a specialized remote agent.
  • A remote agent is owned by another service, team, vendor, or runtime.
  • A backend, browser app, or another A2A-compatible system needs programmatic access to a Mastra agent.
  • Long-running remote work needs task IDs, status updates, artifacts, cancellation, resubscription, or push notifications.

How A2A works
Direct link to How A2A works

A2A uses an agent card for discovery. The card is a JSON document served from a well-known URL. It describes the remote agent and includes the execution URL that accepts A2A JSON-RPC requests.

When using the default Mastra Server apiPrefix of /api, an agent registered as weather-agent exposes:

  • Agent card: /api/.well-known/weather-agent/agent-card.json
  • Execution endpoint: /api/a2a/weather-agent

An agent card includes fields like the agent name, description, endpoint URL, provider, capabilities, security metadata, and skills:

agent-card.json
{
"protocolVersion": "0.3.0",
"name": "Weather Agent",
"description": "Provides weather information.",
"url": "https://agent.example.com/api/a2a/weather-agent",
"version": "1.0",
"provider": {
"organization": "Acme",
"url": "https://acme.example.com"
},
"capabilities": {
"streaming": true,
"pushNotifications": true,
"stateTransitionHistory": false
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": [
{
"id": "weather",
"name": "weather",
"description": "Gets weather conditions for a location.",
"tags": ["tool"]
}
]
}

A2A represents work as messages and tasks. Messages carry text, file, or structured data parts.

Tasks are stateful units of work with IDs and lifecycle states. Clients can follow long-running work and send follow-up turns. They can also cancel work or resubscribe after a disconnect.

Protocol versions
Direct link to Protocol versions

Mastra supports A2A Protocol v0.3 and v1.0 on the same agent card and execution URLs. The A2A-Version request header selects the wire protocol:

  • Missing, empty, or 0.3: Uses the existing v0.3 API.
  • 1.0: Uses the v1.0 API.
  • Any other value: Returns a VersionNotSupported protocol error.

Existing A2AAgent and MastraClient.getA2A() integrations continue to use v0.3. Use MastraClient.getA2AV1() for v1.0 requests. The v1 client sends A2A-Version: 1.0 automatically and adds the tasks/list operation.

Import v1.0 protocol types and codecs from @mastra/core/a2a/v1. The existing @mastra/core/a2a/client export remains on v0.3.

Get started
Direct link to Get started

A2A has two common paths in Mastra:

  • Consume a remote A2A agent as a Mastra subagent with A2AAgent.
  • Send requests to a Mastra A2A endpoint with MastraClient.getA2A().

Use A2AAgent when another Mastra agent should delegate work to a remote agent. Use the client SDK when application code needs to call an A2A-enabled Mastra endpoint directly.

Consume A2A agents as subagents
Direct link to Consume A2A agents as subagents

Use A2AAgent to wrap a remote A2A agent, then add it to a parent agent with the supervisor agents pattern. Pass an explicit agent card URL when the remote server hosts multiple agents or uses a custom well-known path.

src/mastra/agents/support-agent.ts
import { Agent } from '@mastra/core/agent'
import { A2AAgent } from '@mastra/core/a2a'

const remoteWeatherAgent = new A2AAgent({
url: 'https://weather.example.com/api/.well-known/weather-agent/agent-card.json',
headers: {
Authorization: `Bearer ${process.env.WEATHER_AGENT_TOKEN}`,
},
})

export const supportAgent = new Agent({
id: 'support-agent',
name: 'Support Agent',
instructions: 'Answer user questions and delegate weather questions when needed.',
model: 'openai/gpt-5.6-sol',
agents: {
remoteWeatherAgent,
},
})

If url points to a domain, A2AAgent fetches the agent card from /.well-known/agent-card.json. Use a domain URL for single-agent servers that follow that discovery path. For multi-agent servers, pass the full card URL, such as https://agent.example.com/api/.well-known/weather-agent/agent-card.json.

During execution, A2AAgent:

  • Fetches and caches the remote agent card.
  • Reads the execution URL and capabilities from the card.
  • Calls message/send for non-streaming runs or message/stream when streaming is supported.
  • Converts remote messages, tasks, artifacts, and status updates into Mastra subagent results.
  • Supports resumeGenerate() and resumeStream() when the remote task requires follow-up input or resubscription.

If the remote card doesn't advertise streaming support, A2AAgent.stream() falls back to the non-streaming generate path and returns a buffered stream result.

Send requests with the client SDK
Direct link to Send requests with the client SDK

Use MastraClient.getA2A() when you want application code to call an A2A-enabled Mastra agent. Configure baseUrl for the server origin and apiPrefix when the server doesn't use the default /api prefix.

src/a2a-client.ts
import { MastraClient } from '@mastra/client-js'

const client = new MastraClient({
baseUrl: 'https://agent.example.com',
headers: {
Authorization: `Bearer ${process.env.AGENT_API_TOKEN}`,
},
})

const a2a = client.getA2A('weather-agent')
const card = await a2a.getAgentCard()

console.log(card.name, card.capabilities)

Use sendMessageStream() to send a message and receive task status and artifact updates over Server-Sent Events (SSE):

src/a2a-client.ts
const stream = a2a.sendMessageStream({
message: {
kind: 'message',
role: 'user',
messageId: crypto.randomUUID(),
parts: [{ kind: 'text', text: "What's the weather in Prague?" }],
},
})

for await (const event of stream) {
if (event.kind === 'artifact-update') {
console.log(event.artifact.parts)
}
}

If a stream disconnects while a task is still running, use resubscribeTask() to receive live updates for the in-progress task:

src/a2a-client.ts
const updates = a2a.resubscribeTask({
id: 'task-123',
})

for await (const event of updates) {
console.log(event)
}

Use the v1.0 client
Direct link to Use the v1.0 client

Use getA2AV1() to opt into the A2A v1.0 wire protocol. The protocol package provides codecs for creating v1 request values from JSON-shaped input:

src/a2a-v1-client.ts
import { ListTasksRequest } from '@mastra/core/a2a/v1'
import { MastraClient } from '@mastra/client-js'

const client = new MastraClient({
baseUrl: 'https://agent.example.com',
})

const a2a = client.getA2AV1('weather-agent')
const response = await a2a.listTasks(
ListTasksRequest.fromJSON({
contextId: 'customer-support',
pageSize: 20,
}),
)

for (const task of response.tasks) {
console.log(task.id, task.status)
}

The v1.0 client supports getAgentCard(), sendMessage(), sendMessageStream(), getTask(), listTasks(), cancelTask(), and resubscribeTask().

Configure subagent calls
Direct link to Configure subagent calls

A2AAgent accepts request options for authenticated or constrained environments:

src/mastra/agents/support-agent.ts
import { A2AAgent } from '@mastra/core/a2a'

const remoteWeatherAgent = new A2AAgent({
url: 'https://weather.example.com/api/.well-known/weather-agent/agent-card.json',
headers: {
Authorization: `Bearer ${process.env.WEATHER_AGENT_TOKEN}`,
},
retries: 2,
backoffMs: 250,
maxBackoffMs: 1000,
timeoutMs: 30_000,
})

You can also pass credentials, fetch, and abortSignal when the runtime needs custom fetch behavior or request cancellation.

Human-in-the-loop
Direct link to Human-in-the-loop

A2A models human-in-the-loop (HITL) work with the input-required task state. When a task pauses for input, the client provides the missing input by sending a follow-up message with the same taskId, and the server continues the task.

Mastra maps its agent suspension model to this state in both directions:

  • As a server: when an exposed agent suspends, the task transitions to input-required. This includes suspensions caused by tool approval or a tool that calls suspend(). The task status message includes a text prompt and a data part with the structured suspendPayload and resumeSchema. A follow-up message/send or message/stream request with the same taskId resumes the suspended run with the provided input.
  • As a client: when a remote task reaches input-required or auth-required, A2AAgent returns a suspended result with finishReason: 'suspended' and a suspendPayload. Calling resumeGenerate() or resumeStream() sends the input or credentials back to the remote task with the original taskId.
src/a2a-hitl.ts
import { A2AAgent } from '@mastra/core/a2a'

const agent = new A2AAgent({
url: 'https://agent.example.com/api/.well-known/booking-agent/agent-card.json',
})

const result = await agent.generate('Book a flight to Paris', { runId: 'run-1' })

if (result.finishReason === 'suspended') {
// Inspect result.suspendPayload, collect input from a human,
// then resume the remote task.
const resumed = await agent.resumeGenerate({ approved: true }, { runId: 'run-1' })
console.log(resumed.text)
}

Follow-up messages for an input-required task can carry the resume data as a structured data part, or as JSON or plain text in a text part.

When a resumed run requires additional input, the task returns to input-required and the flow repeats. Resuming a suspended run requires storage configured on the Mastra server so the suspended run state can be restored across requests.

note

A2A task records live in an in-memory store, so a paused task can only be resumed by the same server process that suspended it. A server restart or a horizontally scaled deployment without sticky routing loses the task record, and a follow-up message fails with a task-not-found error.

Push notifications
Direct link to Push notifications

Mastra supports A2A push notifications for remote agents that advertise capabilities.pushNotifications. Use push notifications when a client can't keep a stream open, or when a long-running task should update a callback URL after the original request ends.

After a client has a task ID, it can register a callback URL for that task:

src/a2a-client.ts
await a2a.setTaskPushNotificationConfig({
taskId: 'task-123',
pushNotificationConfig: {
url: 'https://app.example.com/a2a/tasks',
token: process.env.A2A_WEBHOOK_TOKEN,
},
})

Mastra Server sends the current task snapshot to registered callbacks when the task reaches completed, failed, canceled, rejected, input-required, or auth-required. Push notification delivery is best-effort. Protect callback URLs, validate notification tokens, and avoid exposing internal network targets as push notification destinations.

Push notification configurations are stored in memory and must be registered again after a server restart.

Sign and verify agent cards
Direct link to Sign and verify agent cards

Mastra supports signed A2A agent cards so clients can verify that a discovered card came from a trusted publisher and wasn't changed in transit. Configure signing on the Mastra server that exposes the remote agent:

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'

export const mastra = new Mastra({
server: {
a2a: {
agentCardSigning: {
privateKey: process.env.A2A_AGENT_CARD_PRIVATE_KEY!,
protectedHeader: {
alg: 'ES256',
kid: 'agent-card-key',
},
},
},
},
})

When signing is configured, Mastra includes a signatures array on the agent card. Client verification is opt-in, and unsigned cards still return unchanged.

Verify a signed card with MastraClient.getA2A():

src/a2a-client.ts
const card = await a2a.getAgentCard({
verifySignature: {
algorithms: ['ES256'],
keyProvider: async ({ kid, jku }) => {
return fetchTrustedPublicJwk({ kid, jku })
},
},
})

if (!card.signatures?.length) {
throw new Error('Expected a signed A2A agent card.')
}

Use client-side signature verification when a client must enforce trusted keys before calling the remote agent.

Verify subagent cards
Direct link to Verify subagent cards

Use verifyAgentCard when a parent agent should validate a remote agent before delegating work to it. The verification hook receives the fetched agent card and context about where and when it was fetched.

src/mastra/agents/support-agent.ts
import { A2AAgent } from '@mastra/core/a2a'

const remoteWeatherAgent = new A2AAgent({
url: 'https://weather.example.com/api/.well-known/weather-agent/agent-card.json',
verifyAgentCard: {
verify: async (card, context) => {
if (card.provider?.organization !== 'Weather Inc') {
throw new Error(`Unexpected provider for ${context.cardUrl}`)
}
},
},
})

Use this hook to enforce expected providers, expected endpoints, certificate-bound identities, signed cards, or other trust requirements before a parent agent delegates to the remote agent.