# Interop

> Source: https://openwop.dev/spec/v2/core/interop.html · OpenWOP, the open-source protocol for multi-agent workflow orchestration. CC BY 4.0.

> **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](https://openwop.dev/spec/v2/core/capabilities.html); the peer identity is the Subject of [identity.md](https://openwop.dev/spec/v2/core/identity.html).

## 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 `supportedTransports` MUST fail validation against `schemas/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, refusals included, MUST emit a content-free `negotiation.decided` record (`run-event-payloads.schema.json#/$defs/negotiationDecided`): on its run's log, or, with no run, as the caller's durable host event. `peerDigest` MUST be the peer origin's digest; `requested` MUST carry any version the requester named. It witnesses `a2a-version-no-silent-downgrade` and `mcp-version-no-silent-downgrade`. A host serving either protocol inbound (`a2a.agentCardUrl`, `mcp.serverUrls`, `mcp.serverMount`) MUST list it as a durable `hostEvents` type.

**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`; its record is `accepted` with a `version` other than `requested`.

## 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 `requires` facet 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 `getRun` MUST be answered exactly as a nonexistent one, including a tenant mismatch REST refuses `403`.
- `ListTasks` MUST return only runs `listRuns` would return to the same Subject, whether or not `runList` is advertised.
- `contextId`, `tenant` and `_meta` never select a tenant, workspace or principal.

**A2A multi-turn (A2A §3.4.3).**

- A message carrying `taskId` without `contextId` MUST be answered with the task's `contextId`.
- A message whose `contextId` is 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`; `TaskNotFoundError` is for unknown, purged and unreadable tasks.

**A2A error details.** A host:

- on a 1.0 `JSONRPC` interface, MUST make an A2A error's `error.data` an array of objects each carrying `@type`, including exactly one `type.googleapis.com/google.rpc.ErrorInfo` whose `reason` is the map row's `reason` and whose `domain` is `a2a-protocol.org`;
- on an `HTTP+JSON` interface, MUST answer A2A §11.6's `google.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 `TaskNotFoundError` details between an unknown task and an unreadable one, apart from an echo of the requested id;
- on `VersionNotSupportedError`, SHOULD set `metadata.supportedVersions` to a comma-joined list; a client falls back to the card's `supportedInterfaces[].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/call` that declared the extension with `CreateTaskResult` whenever the run is not terminal when the host answers, never with `InputRequiredResult`;
- MUST use the run's projected `runId` (identity.md §5) as `taskId`, with an opaque segment of at least 128 bits of entropy. A `taskId` is 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/cancelled` naming the request, MUST cancel the run as `cancelRun` would, with `run.cancelled.reason` `mcp-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/cancelled` except to end a `subscriptions/listen` stream.

## 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.md` `retryPolicy` semantics.
- The body is an A2A 1.0 `StreamResponse` sent as `application/a2a+json`, carrying `Authorization: {scheme} {credentials}` from the config. When only `token` is set, a host SHOULD send it as `Authorization: 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 `replay` fork 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 `configId` that is not that task's, MUST answer exactly as for an unknown id, apart from the JSON-RPC `id`.
- A `configId` MUST 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:

- `name` is the entry's `persona`, `version` its `packVersion`, `description` its `description` or else `label`;
- `supportedInterfaces[]` are the host card's interfaces, each carrying `tenant: R`;
- `capabilities` and `securitySchemes` equal 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 `R` MUST be authenticated and authorized as `GET /agents/{agentId}` is, before `R` is resolved.
- For an `R` naming an agent outside the caller's inventory, every A2A operation MUST return what it returns for an `R` the host never minted, apart from the JSON-RPC `id`.
- The public card at `agentCardUrl` MUST NOT list any `R`.
- `R` is a `tenant` value 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 `grpc` capability block.
- `api/v2/openapi.yaml` and 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` (unprefixed `traceparent`, and `tracestate` when present; MCP 2026-07-28 `_meta`, SEP-414) or in the HTTP `traceparent` header, and SHOULD use `_meta`, the only carrier on stdio;
- into an A2A message MUST carry it in `Message.metadata.openwop.traceparent` and `.tracestate` or 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.*
