← back to A2a Lab
add working A2A echo demo (SDK server + raw JSON-RPC client), verified end-to-end
4dc181fbf642b40dbabaa4192fc85bd51f3b1335 · 2026-08-01 19:32:47 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M README.mdA client.pyM requirements.txtA server.py
Diff
commit 4dc181fbf642b40dbabaa4192fc85bd51f3b1335
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Aug 1 19:32:47 2026 -0700
add working A2A echo demo (SDK server + raw JSON-RPC client), verified end-to-end
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
README.md | 32 ++++++++++++++++++
client.py | 62 +++++++++++++++++++++++++++++++++++
requirements.txt | 4 +++
server.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 197 insertions(+)
diff --git a/README.md b/README.md
index dd0f62a..6e7b355 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,38 @@ python smoke_test.py # Python SDK import + version check
node smoke.js # JS SDK import check
```
+## Working demo (verified end-to-end)
+
+An "Echo Agent" that upper-cases your text, exercising the full A2A round-trip:
+
+```bash
+cd ~/Projects/a2a-lab
+. .venv/bin/activate
+python server.py # terminal 1 — listens on 127.0.0.1:41241
+python client.py "hello" # terminal 2 — discovers the card + sends a message
+```
+
+- `server.py` — SDK stack: `EchoAgentExecutor(AgentExecutor)` → `DefaultRequestHandler`
+ + `InMemoryTaskStore` + a proto `AgentCard`, served via Starlette
+ (`create_agent_card_routes` + `create_jsonrpc_routes`) under uvicorn.
+- `client.py` — deliberately a **raw JSON-RPC httpx** call (no SDK client), so it
+ self-documents the wire protocol: GET the card → POST `SendMessage`.
+
+Verified output: client gets `{"result": {"message": {"parts": [{"text": "echo: HELLO"}]}}}`.
+
+### A2A wire contract in a2a-sdk 1.1.2 (proto-native — differs from old docs!)
+
+1. **Discovery:** `GET /.well-known/agent-card.json` (proto→JSON is **camelCase**:
+ `protocolBinding`, `supportedInterfaces`).
+2. **Method names are gRPC-style PascalCase** — `SendMessage` (NOT `message/send`),
+ `GetTask`, `CancelTask`, `SendStreamingMessage` (SSE), etc.
+3. **Params are a protobuf `SendMessageRequest`** parsed via `ParseDict`:
+ `{"message": {"messageId": "...", "role": "ROLE_USER", "parts": [{"text": "..."}]}}`
+ — enum values are proto **names** (`ROLE_USER`/`ROLE_AGENT`), fields camelCase.
+4. **Protocol version travels in the `A2A-Version` HTTP header** — a MISSING header
+ silently defaults to legacy `0.3` and the 1.0 handler rejects it with `-32009`.
+ Always send `A2A-Version: 1.0`.
+
## Next steps (not done — future work)
- Build a minimal A2A *server* that exposes one of Steve's cabinet agents (or the
diff --git a/client.py b/client.py
new file mode 100644
index 0000000..61e6a4b
--- /dev/null
+++ b/client.py
@@ -0,0 +1,62 @@
+"""Minimal A2A (Agent2Agent) demo client — spec-compliant raw JSON-RPC over httpx.
+
+Deliberately does NOT use the SDK client, so it self-documents the A2A wire protocol:
+ 1. DISCOVER — GET the agent card from /.well-known/agent-card.json
+ 2. INVOKE — POST a JSON-RPC 2.0 `message/send` with a text message
+ 3. READ — print the agent's reply
+
+Run (server must be up): . .venv/bin/activate && python client.py
+"""
+from __future__ import annotations
+
+import json
+import sys
+import uuid
+
+import httpx
+
+BASE = "http://127.0.0.1:41241"
+
+
+def discover(base: str) -> dict:
+ card = httpx.get(f"{base}/.well-known/agent-card.json", timeout=10).json()
+ print(f"① DISCOVERED agent card → name={card.get('name')!r} version={card.get('version')!r}")
+ ifaces = card.get("supportedInterfaces") or card.get("supported_interfaces") or []
+ if ifaces:
+ print(f" interface → {ifaces[0]}")
+ return card
+
+
+def send_message(base: str, text: str) -> dict:
+ # a2a-sdk 1.1.2 uses gRPC-style method names (SendMessage) and parses params
+ # as a protobuf SendMessageRequest (camelCase fields, enum-name roles).
+ payload = {
+ "jsonrpc": "2.0",
+ "id": str(uuid.uuid4()),
+ "method": "SendMessage",
+ "params": {
+ "message": {
+ "messageId": str(uuid.uuid4()),
+ "role": "ROLE_USER",
+ "parts": [{"text": text}],
+ }
+ },
+ }
+ print(f"② SEND SendMessage → {text!r}")
+ # The server defaults a missing A2A-Version header to legacy '0.3'; signal 1.0.
+ headers = {"A2A-Version": "1.0"}
+ resp = httpx.post(f"{base}/", json=payload, headers=headers, timeout=30)
+ return resp.json()
+
+
+def main() -> int:
+ text = sys.argv[1] if len(sys.argv) > 1 else "hello a2a"
+ discover(BASE)
+ result = send_message(BASE, text)
+ print("③ RESPONSE:")
+ print(json.dumps(result, indent=2))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/requirements.txt b/requirements.txt
index b6e0c7f..e9a6bbd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ anyio==4.14.2
certifi==2026.7.22
cffi==2.1.0
charset-normalizer==3.4.9
+click==8.4.2
cryptography==50.0.0
google-api-core==2.33.0
google-auth==2.56.2
@@ -22,6 +23,9 @@ pycparser==3.0
pydantic==2.13.4
pydantic_core==2.46.4
requests==2.34.2
+sse-starlette==3.4.6
+starlette==1.3.1
typing-inspection==0.4.2
typing_extensions==4.16.0
urllib3==2.7.0
+uvicorn==0.52.1
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..fc145ab
--- /dev/null
+++ b/server.py
@@ -0,0 +1,99 @@
+"""Minimal A2A (Agent2Agent) demo server — a2a-sdk 1.1.2, current proto API.
+
+An "Echo Agent": receives a text message and returns it upper-cased. It publishes
+a standard A2A agent card at /.well-known/agent-card.json and serves the JSON-RPC
+A2A endpoint at / . Proves the SDK server stack (AgentExecutor + DefaultRequestHandler
++ InMemoryTaskStore + Starlette route builders).
+
+Run: . .venv/bin/activate && python server.py (listens on 127.0.0.1:41241)
+"""
+from __future__ import annotations
+
+import uuid
+
+import uvicorn
+from starlette.applications import Starlette
+
+from a2a.types import (
+ AgentCard,
+ AgentInterface,
+ AgentCapabilities,
+ AgentSkill,
+ Message,
+ Part,
+ Role,
+)
+from a2a.server.agent_execution import AgentExecutor, RequestContext
+from a2a.server.events import EventQueue
+from a2a.server.request_handlers import DefaultRequestHandler
+from a2a.server.tasks import InMemoryTaskStore
+from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
+
+HOST = "127.0.0.1"
+PORT = 41241
+
+
+class EchoAgentExecutor(AgentExecutor):
+ """The agent's brain: read the user's text, reply with it upper-cased."""
+
+ async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
+ # get_user_input() concatenates the text parts of the incoming message.
+ try:
+ user_text = context.get_user_input() or ""
+ except Exception:
+ user_text = ""
+ reply = Message(
+ message_id=str(uuid.uuid4()),
+ role=Role.ROLE_AGENT,
+ parts=[Part(text=f"echo: {user_text.upper()}")],
+ )
+ await event_queue.enqueue_event(reply)
+
+ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
+ raise NotImplementedError("Echo agent does not support cancellation.")
+
+
+def build_agent_card() -> AgentCard:
+ return AgentCard(
+ name="Echo Agent",
+ description="Minimal A2A demo agent — returns your text upper-cased.",
+ version="0.1.0",
+ supported_interfaces=[
+ AgentInterface(
+ url=f"http://{HOST}:{PORT}/",
+ protocol_binding="JSONRPC",
+ protocol_version="1.0",
+ )
+ ],
+ capabilities=AgentCapabilities(streaming=False, push_notifications=False),
+ default_input_modes=["text/plain"],
+ default_output_modes=["text/plain"],
+ skills=[
+ AgentSkill(
+ id="echo",
+ name="Echo",
+ description="Uppercase-echo the input text.",
+ tags=["demo", "echo"],
+ examples=["hello world"],
+ )
+ ],
+ )
+
+
+def build_app() -> Starlette:
+ card = build_agent_card()
+ handler = DefaultRequestHandler(
+ agent_executor=EchoAgentExecutor(),
+ task_store=InMemoryTaskStore(),
+ agent_card=card,
+ )
+ routes = [
+ *create_agent_card_routes(card), # GET /.well-known/agent-card.json
+ *create_jsonrpc_routes(handler, "/"), # POST / (A2A JSON-RPC)
+ ]
+ return Starlette(routes=routes)
+
+
+if __name__ == "__main__":
+ print(f"A2A Echo Agent on http://{HOST}:{PORT} (card: /.well-known/agent-card.json)")
+ uvicorn.run(build_app(), host=HOST, port=PORT, log_level="warning")
← f3cea00 install A2A SDKs (python a2a-sdk 1.1.2 + @a2a-js/sdk), smoke
·
back to A2a Lab
·
add read-only A2A bridge over the live tk cross-agent DM log 89535d8 →