A2A → MCP invocation packet
June 3, 2026 · View on GitHub
This note records the acceptance packet for AIP-1 §7.4: A2A-style agent cards that point at an MCP Streamable HTTP endpoint should carry the full invocation contract inline.
Problem trace
A2A directory crawlers commonly begin at /.well-known/agent-card.json, extract the top-level MCP URL, and POST to it without reading /.well-known/oabp.json, /agents.txt, or this repository.
Observed loop from issue #22:
GET /.well-known/agent-card.json 200 ← reads card, extracts url: ".../mcp"
POST /mcp 400 ← missing JSON-RPC initialize body
GET /.well-known/agent-card.json 200 ← returns to the card looking for a hint
Moving the recipe to /agents.txt was not enough: a crawler fetched the text recipe and later repeated the same card → /mcp 400 loop. The machine-readable recipe has to live in the card itself.
Before: URL only
A minimal A2A card tells a crawler where to connect but not how to connect:
{
"name": "AIGEN Protocol",
"url": "https://cryptogenesis.duckdns.org/mcp",
"capabilities": {"streaming": true}
}
A generic crawler cannot infer the required MCP Streamable HTTP sequence from that shape.
After: card-side invocation contract
The live card now includes transport.protocols[0].handshake:
{
"transport": {
"primary": "mcp-streamable-http",
"protocols": [
{
"id": "mcp-streamable-http",
"url": "https://cryptogenesis.duckdns.org/mcp",
"handshake": {
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18"
},
"body": {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "<your-agent-name>", "version": "0.1.0"}
}
},
"responseSessionHeader": {"name": "Mcp-Session-Id"},
"postInitializeNotification": {
"method": "POST",
"headers": {"Mcp-Session-Id": "<value-from-initialize-response>"},
"body": {"jsonrpc": "2.0", "method": "notifications/initialized"}
},
"exampleNextCall": {
"method": "POST",
"headers": {"Mcp-Session-Id": "<value-from-initialize-response>"},
"body": {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
}
}
},
{
"id": "oabp-rest-readonly",
"endpoints": [
{"path": "/api/missions", "method": "GET"},
{"path": "/api/missions/{id}", "method": "GET"},
{"path": "/openapi.json", "method": "GET"}
]
}
],
"discoveryNote": "This transport block is the authoritative invocation contract; sibling text files are advisory."
}
}
Curl replay transcript
The same sequence was replayed against the live endpoint while preparing this spec update:
initialize 200 session_header True content_type text/event-stream
initialized 202 bytes 0
tools/list 200 content_type text/event-stream bytes 41467
delete 200 bytes 0
The important details for clients:
initializereturns anMcp-Session-Idresponse header.- The client must echo that header on
notifications/initialized. - The client must keep echoing it on
tools/list,tools/call, and later requests. DELETE /mcpwith the same session header releases the session.
Error contract
If a crawler POSTs to /mcp without initialize, the card advertises a machine-readable JSON-RPC error shape under transport.protocols[0].errorShape.missingInitialize:
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32600,
"message": "Invalid Request: server must receive a JSON-RPC 'initialize' before any other method.",
"data": {
"expectedMethod": "initialize",
"transport": "streamable-http",
"recipeUrl": "https://cryptogenesis.duckdns.org/.well-known/agent-card.json#/transport/protocols/0/handshake"
}
}
}
This lets a failed crawler self-repair by following the JSON Pointer back to the copyable handshake object.
Advisory text files
/agents.txt and /llms.txt remain useful for humans and RAG-style readers, but they are not the authoritative invocation contract. Machine clients starting from agent-card.json should not be required to fetch sibling text files before they can make the first successful MCP request.