Three implementations of the minimum runnable A2A surface: an agent-card at the well-known path plus a JSON-RPC handler that serves one skill. Pick your stack. Each tab ships ~40 lines of code, a copy button, and the two-line run command. Skills, auth and x402 are additions on top of this floor.
# server.py — minimum A2A server. FastAPI + uvicorn. No external A2A SDK. from fastapi import FastAPI, Request from fastapi.responses import JSONResponse app = FastAPI() AGENT_CARD = { "$schema": "https://a2a-protocol.org/schemas/agent-card/v1.json", "name": "hello-a2a", "version": "0.1.0", "description": "Minimum runnable A2A server. One skill: hello.", "url": "http://localhost:8080", "endpoints": {"a2a": "http://localhost:8080/api/a2a"}, "auth": {"type": "none"}, "capabilities": {"streaming": False}, "skills": [{ "id": "hello", "name": "Hello", "description": "Greet by name. Returns a string.", "inputModes": ["text/plain"], "outputModes": ["text/plain"], }], } @app.get("/.well-known/agent-card.json") async def agent_card(): return AGENT_CARD @app.post("/api/a2a") async def a2a(req: Request): body = await req.json() rpc_id = body.get("id") method = body.get("method") params = body.get("params", {}) if method == "message/send": text = (params.get("message", {}).get("parts", [{}])[0] or {}).get("text", "") result = {"parts": [{"kind": "text", "text": f"hello, {text or 'world'}!"}]} return {"jsonrpc": "2.0", "id": rpc_id, "result": result} return JSONResponse({"jsonrpc": "2.0", "id": rpc_id, "error": {"code": -32601, "message": "method not found"}}, status_code=200)
pip install fastapi uvicorn
uvicorn server:app --port 8080
curl http://localhost:8080/.well-known/agent-card.json
JSON-RPC call: POST /api/a2a with { method: "message/send", params: { message: { parts: [{ text: "world" }] } } }
// server.js — minimum A2A server. Express. No external A2A SDK. const express = require("express"); const app = express(); app.use(express.json()); const AGENT_CARD = { "$schema": "https://a2a-protocol.org/schemas/agent-card/v1.json", name: "hello-a2a", version: "0.1.0", description: "Minimum runnable A2A server. One skill: hello.", url: "http://localhost:8080", endpoints: { a2a: "http://localhost:8080/api/a2a" }, auth: { type: "none" }, capabilities: { streaming: false }, skills: [{ id: "hello", name: "Hello", description: "Greet by name. Returns a string.", inputModes: ["text/plain"], outputModes: ["text/plain"] }] }; app.get("/.well-known/agent-card.json", (_, res) => res.json(AGENT_CARD)); app.post("/api/a2a", (req, res) => { const { id, method, params = {} } = req.body || {}; if (method === "message/send") { const text = params?.message?.parts?.[0]?.text || "world"; return res.json({ jsonrpc: "2.0", id, result: { parts: [{ kind: "text", text: `hello, ${text}!` }] } }); } res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: "method not found" } }); }); app.listen(8080, () => console.log("a2a · http://localhost:8080"));
npm init -y && npm install express
node server.js
curl http://localhost:8080/.well-known/agent-card.json
Same JSON-RPC envelope: POST /api/a2a { jsonrpc: "2.0", id: 1, method: "message/send", params: ... }
// worker.ts — minimum A2A server on Cloudflare Workers. No external A2A SDK. const AGENT_CARD = { "$schema": "https://a2a-protocol.org/schemas/agent-card/v1.json", name: "hello-a2a", version: "0.1.0", description: "Minimum runnable A2A server on a Worker.", url: "https://hello-a2a.workers.dev", endpoints: { a2a: "https://hello-a2a.workers.dev/api/a2a" }, auth: { type: "none" }, capabilities: { streaming: false }, skills: [{ id: "hello", name: "Hello", description: "Greet by name. Returns a string.", inputModes: ["text/plain"], outputModes: ["text/plain"] }] }; const CORS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Accept" }; export default { async fetch(req: Request): Promise<Response> { const { pathname } = new URL(req.url); if (req.method === "OPTIONS") return new Response(null, { headers: CORS }); if (pathname === "/.well-known/agent-card.json") { return Response.json(AGENT_CARD, { headers: CORS }); } if (pathname === "/api/a2a" && req.method === "POST") { const { id, method, params = {} } = await req.json() as any; if (method === "message/send") { const text = params?.message?.parts?.[0]?.text || "world"; return Response.json({ jsonrpc: "2.0", id, result: { parts: [{ kind: "text", text: `hello, ${text}!` }] } }, { headers: CORS }); } return Response.json({ jsonrpc: "2.0", id, error: { code: -32601, message: "method not found" } }, { headers: CORS }); } return new Response("not found", { status: 404, headers: CORS }); } };
npm create cloudflare@latest hello-a2a -- --type=hello-world-ts
cd hello-a2a && cp ../worker.ts src/index.ts
npx wrangler deploy
CORS headers are non-optional for Workers: WebMCP browsers will need them on the well-known route too.