← labs · 01 · handshake
lab 01 · ~5 min

The A2A handshake, step by step.

One real client. One real server. Six steps. Click RUN HANDSHAKE and watch a browser tab discover an agent card, list tools, and call a public method on immersivecommons.com. If the fetch is blocked by CORS, the same payload replays from cache so the animation never stalls.

transport · origin immersivecommons.com
client · your browser idle
(client ready — press RUN HANDSHAKE)
server · immersivecommons.com idle
(server idle — awaiting request)
What you just saw. The well-known agent card is the entry point: one HTTPS GET surfaces the entire surface area. The JSON-RPC call you fired hits the MCP endpoint declared inside that card — no out-of-band config. This is the SOTA discovery contract: /.well-known/agent-card.json + an endpoint, talking JSON-RPC 2.0.
fetch source · ~30 lines
// 1. Discover the agent card (one HTTPS GET).
const card = await fetch(
  "https://www.immersivecommons.com/.well-known/agent-card.json"
).then(r => r.json());

// 2. The card tells you where the MCP endpoint lives.
const mcpUrl = card.endpoints.mcp;

// 3. Initialize an MCP session over Streamable HTTP.
//    Accept BOTH application/json and text/event-stream — the server picks.
const init = await fetch(mcpUrl, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json, text/event-stream"
  },
  body: JSON.stringify({
    jsonrpc: "2.0", id: 1,
    method: "initialize",
    params: {
      protocolVersion: "2025-03-26",
      capabilities: {},
      clientInfo: { name: "workshop-demo", version: "0.1" }
    }
  })
}).then(r => r.json());

// 4. Call a public tool — no auth needed.
const result = await fetch(mcpUrl, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json, text/event-stream"
  },
  body: JSON.stringify({
    jsonrpc: "2.0", id: 2,
    method: "tools/call",
    params: { name: "ic_signal_get_latest", arguments: {} }
  })
}).then(r => r.json());