Status: Stable.
Normative home:
a2a,mcp.
Why this exists
A2A and MCP are embedded protocols: a host advertises them, negotiates a version, and records every negotiation. Capability shapes are in capabilities.md; the peer identity is the Subject of identity.md.
REST is the wire
REST and SSE are the wire. A2A and MCP are compositions over it, advertised by their own facets and nothing else.
- A host MUST NOT advertise a transport list.
- A discovery document carrying
supportedTransportsMUST fail validation againstschemas/v2/capabilities.schema.json.
The facets
A host that speaks either protocol MUST advertise the corresponding facet — a2a or mcp — with every required field (spec/v2/facets/a2a.schema.json, spec/v2/facets/mcp.schema.json).
| Facet field | A2A (a2a) | MCP (mcp) | Rule |
|---|---|---|---|
| Offered versions | versions[] (major.minor) | revisions[] (dates) | REQUIRED, at least one entry |
| Default | preferredVersion | preferredVersion | REQUIRED; served when the peer names none |
| Floor | minimumVersion | minimumRevision | REQUIRED; below it negotiation fails closed |
| Freshness | refreshedAt | refreshedAt | REQUIRED; see the refresh SLA |
| Profiles | profiles[] a2a-<major.minor> | profiles[] mcp-<date> | no -legacy suffix |
| Protocol-specific | agentCardUrl, streaming, pushNotifications, durableTasks | features[], serverUrls[], serverMount.transports[] (stdio | streamable-http), mrtr.maxRounds | optional |
mcp.serverMount.transports[] is the MCP server's own transport enum; it is not a host transport advertisement.
Legacy profiles are absent
The profiles[] item patterns admit no -legacy suffix, so a2a-0.3-legacy and mcp-2025-06-18-legacy are not valid profile ids. A host that still speaks a legacy version does so as a private, non-advertised behavior.
When no A2A-Version header is present, a host MUST serve the agent card of preferredVersion.
Negotiation is a protocol
Authentication. A version-negotiation exchange on either protocol MUST be authenticated: the peer identity is the caller's Subject (identity.md) or the host's own outbound identity. An unauthenticated exchange MUST NOT lower the negotiated version below preferredVersion.
The floor. A negotiation that would land below minimumVersion / minimumRevision MUST fail closed with interop_version_unsupported (spec/v2/errors.json), whether or not host policy permits an explicit downgrade above the floor.
The audit event. Every negotiation outcome, including the refused one, MUST emit a negotiation.decided event on the host's own event log:
{ "protocol": "a2a" | "mcp", "peer": "<origin digest>", "requested": "…",
"negotiated": "…" | null, "outcome": "accepted" | "downgraded" | "refused", "reason": "…" }
The event is content-free: peer MUST be a digest of the peer origin, never the origin in clear. The event is the normative witness of the invariants a2a-version-no-silent-downgrade and mcp-version-no-silent-downgrade.
The refresh SLA. A host MUST re-evaluate its advertised versions[] / revisions[] against the upstream registry within the window its refreshedAt declares, and that window MUST NOT exceed 90 days. An advertisement older than its window is non-conformant.
Downgrade above the floor. A host MAY accept an authenticated request for a version between the floor and preferredVersion; the event then reports outcome: downgraded.
The operation mappings
spec/v2/interop-map.json (schema interop-map.schema.json) maps each profile's upstream operations, states, fields and errors to the v2 wire, pinned to an upstream release. A host advertising a profile:
- MUST serve every row it implements as the row states, under the caller's Subject with the authorization, tenant scoping and state of the v2 operation the row names;
- MUST refuse a row whose
requiresfacet it does not advertise with the row's error; - MUST list every feature the map requires for that profile.
What the map does not name is opaque: it MUST round-trip where upstream requires it and MUST NOT become authority, a prompt segment, a tool call or a workflow variable. A patch release that re-maps a row is a map edit; patch numbers are never negotiated.
Isolation. On either interface:
- A task the caller could not read through
getRunMUST be answered exactly as a nonexistent one, including a tenant mismatch REST refuses403. ListTasksMUST return only runslistRunswould return to the same Subject, whether or notrunListis advertised.contextId,tenantand_metanever select a tenant, workspace or principal.
A2A multi-turn (A2A §3.4.3).
- A message carrying
taskIdwithoutcontextIdMUST be answered with the task'scontextId. - A message whose
contextIdis not its task's MUST be refused with its binding's invalid-parameters error and MUST NOT change the run. - A message to a retained terminal task MUST be refused
UnsupportedOperationError;TaskNotFoundErroris for unknown, purged and unreadable tasks.
A2A error details. A host:
- on a 1.0
JSONRPCinterface, MUST make an A2A error'serror.dataan array of objects each carrying@type, including exactly onetype.googleapis.com/google.rpc.ErrorInfowhosereasonis the map row'sreasonand whosedomainisa2a-protocol.org; - on an
HTTP+JSONinterface, MUST answer A2A §11.6'sgoogle.rpc.Status; - MUST NOT answer with the OpenWOP error envelope on an interface URL the card lists, including a refusal before dispatch;
- MUST NOT vary
TaskNotFoundErrordetails between an unknown task and an unreadable one, apart from an echo of the requested id; - on
VersionNotSupportedError, SHOULD setmetadata.supportedVersionsto a comma-joined list; a client falls back to the card'ssupportedInterfaces[].protocolVersion.
A client MUST identify an error by code or by the ErrorInfo reason, MUST accept data as an array, and SHOULD accept a data object carrying reason through 2.x.
MCP tasks and cancellation
A host MAY serve the MCP Tasks extension io.modelcontextprotocol/tasks (revision 2026-07-28) on its server mount. It advertises it in its server/discover capabilities.extensions and by listing extensions in mcp.features[], and nowhere else. A host that advertises it MUST implement the extension as published and the map's mcp.tasks rows, and:
- MUST answer a
tools/callthat declared the extension withCreateTaskResultwhenever the run is not terminal when the host answers, never withInputRequiredResult; - MUST use the run's projected
runId(identity.md §5) astaskId, with an opaque segment of at least 128 bits of entropy. AtaskIdis never a credential; - MUST NOT append to a run's log to answer
tasks/get.
Cancellation. Until the host has sent its whole response to a request that starts or continues a run, the run belongs to that request.
- Before the response is sent, a client disconnect on streamable HTTP, or a stdio
notifications/cancellednaming the request, MUST cancel the run ascancelRunwould, withrun.cancelled.reasonmcp-request-cancelled. - Once the response is sent, a disconnect MUST NOT affect the run; a task ends through
tasks/cancel,cancelRun, or its own terminal state. - A host MUST NOT send
notifications/cancelledexcept to end asubscriptions/listenstream.
The MCP round ceiling
mcp.mrtr.maxRounds (integer, 1–16) is the advertised ceiling on multi-round tool-result rounds. A host MUST refuse an input_required round beyond maxRounds with mcp_mrtr_rounds_exceeded (spec/v2/errors.json). The requestState rules are the map's mcp.mrtr rows.
The durable-task projection
auth-required remains a member of the persisted A2A task state enum (schemas/v2/a2a-task-state.schema.json) for the reverse direction (consuming an external A2A agent). The forward projection MUST emit it, with interruptKind: credential and a status message carrying connectUrl, for a run suspended on a credential interrupt (interrupt.md), and MUST NOT emit it otherwise.
A2A push delivery
A host advertising a2a.pushNotifications treats each push as a webhook egress: webhooks.md §Egress binds at delivery time as well as registration, a 3xx is a failed delivery, and the push credential is bound as security-defaults.md §"Onward hops" states.
- A host MUST attempt each push at least once; a host that retries follows
webhooks.mdretryPolicysemantics. - The body is an A2A 1.0
StreamResponsesent asapplication/a2a+json, carryingAuthorization: {scheme} {credentials}from the config. When onlytokenis set, a host SHOULD send it asAuthorization: Bearer <token>. - A host MUST NOT add an OpenWOP signature.
- Push dead-letters are not visible to A2A clients; a client recovers with
GetTask. - A
replayfork MUST NOT push re-emitted history, and no fork inherits a source run's push configs. - A push-config read or delete on a task the caller cannot read, or naming a
configIdthat is not that task's, MUST answer exactly as for an unknown id, apart from the JSON-RPCid. - A
configIdMUST NOT encode a tenant, workspace or principal. Delete is idempotent.
Per-agent cards
A host advertising a2a.agentCards MUST also offer the a2a-1.0 profile and agents.manifestRuntime, and MUST declare capabilities.extendedAgentCard: true on its public card. It publishes each entry of a caller's agent inventory (GET /agents) as an A2A AgentCard, reached through the entry's a2aTenant: an opaque routing value R the host mints, stable for the agent and host version, that MUST NOT encode a tenant, workspace, or principal.
GetExtendedAgentCard with tenant: R MUST return that agent's card:
nameis the entry'spersona,versionitspackVersion,descriptionitsdescriptionor elselabel;supportedInterfaces[]are the host card's interfaces, each carryingtenant: R;capabilitiesandsecuritySchemesequal the host card's;skills[]holds one skill per workflow the host routes to the agent for this caller.
The card MUST NOT carry anything the inventory entry may not, and does not replace it: degraded[] and memoryDegraded stay on the entry.
Non-disclosure.
- A request carrying
RMUST be authenticated and authorized asGET /agents/{agentId}is, beforeRis resolved. - For an
Rnaming an agent outside the caller's inventory, every A2A operation MUST return what it returns for anRthe host never minted, apart from the JSON-RPCid. - The public card at
agentCardUrlMUST NOT list anyR. Ris atenantvalue under §"The operation mappings" Isolation.
gRPC
gRPC is not part of the core wire. Its document lives at spec/v2/ext/grpc-transport/ with witness: unwitnessable and adoption: none; its requirements are SHOULDs of that extension.
- A host MUST NOT advertise a
grpccapability block. api/v2/openapi.yamland the AsyncAPI document are the only canonical API descriptions.
Trace context
A host that propagates W3C Trace Context:
- into an MCP request MUST carry it in that request's
params._meta(unprefixedtraceparent, andtracestatewhen present; MCP 2026-07-28_meta, SEP-414) or in the HTTPtraceparentheader, and SHOULD use_meta, the only carrier on stdio; - into an A2A message MUST carry it in
Message.metadata.openwop.traceparentand.tracestateor in the HTTP header, and SHOULD use the metadata.
A receiver prefers the in-message value, ignores a malformed one, and MUST NOT derive tenant, principal or scope from either.
Threat model
SECURITY/threat-model-interop.md is the threat model for this document. Peer identity and authorization at the boundary are governed by security-defaults.md; a peer MUST NOT gain authority the caller's Subject does not hold.
Sources: RFCs 0175, 0198, 0207, 0208, 0211, 0214.