A2A Playground
40A local CLI that serves a Vue UI and proxies to an A2A (Agent-to-Agent) agent. Use it to test and interact with A2A agents in your browser. Supports both gRPC and JSON-RPC transports.
Getting Started
Or connect to the hosted endpoint: https://github.com/alis-exchange/a2a-playground
README
a2a-playground
A local CLI that serves a Vue UI and proxies to an A2A (Agent-to-Agent) agent. Use it to test and interact with A2A agents in your browser. Supports both gRPC and JSON-RPC transports.
Quick start
With an A2A agent running (e.g. on port 8080), install and run:
go install github.com/alis-exchange/a2a-playground/cmd/a2a-playground@latest
a2a-playground --agent-url=localhost:8080
The browser opens at http://localhost:3000. Start chatting with your agent.
Installation
From release
No dependencies. Installs the binary for your platform:
curl -fsSL https://raw.githubusercontent.com/alis-exchange/a2a-playground/main/install.sh | bash
Or a specific version:
./install.sh -v v1.0.0
With Go
Requires Go 1.25+:
go install github.com/alis-exchange/a2a-playground/cmd/a2a-playground@latest
Or a specific version:
go install github.com/alis-exchange/a2a-playground/cmd/a2a-playground@v1.0.0
Run without installing (fetches and builds on first use):
# gRPC
go run github.com/alis-exchange/a2a-playground/cmd/a2a-playground@latest --agent-url=localhost:8080
# JSON-RPC
go run github.com/alis-exchange/a2a-playground/cmd/a2a-playground@latest --agent-url=http://localhost:8080/jsonrpc --jsonrpc
Note:
go runandgo installwith a version (e.g.@v1.0.0) only work for releases where the embedded frontend was committed. Use the install script or build from source if a version fails with "no matching files found".
From source
Requires Go 1.25+, Node.js 20+, pnpm, and buf CLI:
make build
This generates protobuf code, builds the frontend, embeds it, and compiles the a2a-playground binary.
Usage
gRPC (default)
a2a-playground --agent-url=localhost:8080
JSON-RPC
a2a-playground --agent-url=http://localhost:8080/jsonrpc --jsonrpc
Flags
| Flag | Default | Description |
|---|---|---|
--agent-url |
localhost:8080 |
Agent endpoint. For gRPC: host:port. For JSON-RPC: full URL (e.g. http://localhost:8080/jsonrpc) |
--grpc |
(when no protocol flag) | Use gRPC transport (default) |
--jsonrpc |
— | Use JSON-RPC over HTTP transport |
--port |
3000 |
HTTP port for the BFF |
--no-open |
false |
Do not open the browser on start |
--dev |
false |
Serve from app/dist on disk instead of embedded files |
Custom headers
Configure authentication and custom headers in the playground UI (key icon in the toolbar). Headers such as Authorization, X-API-Key, and X-Tenant-ID are persisted and forwarded to the agent on every request.
Architecture
┌─────────┐ ┌─────────────────────────────────────┐ ┌────────────────────────────┐
│ Browser │────▶│ BFF (static SPA + Connect proxy) │────▶│ gRPC or JSON-RPC A2A agent │
│ │ │ serves Vue app, proxies A2A RPC │ │ (e.g. localhost:8080) │
└─────────┘ └─────────────────────────────────────┘ └────────────────────────────┘
The BFF (Backend-for-Frontend) serves the static Vue SPA and proxies Connect-RPC requests to your A2A agent. You can use gRPC (default) or JSON-RPC depending on what your agent exposes.
Message flow (Frontend → BFF → Agent)
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Frontend (Vue) │
│ messages.ts → createA2AClient().sendStreamingMessage(req) │
│ a2aClient.ts: agentHeadersInterceptor sets X-A2A-Agent-Headers from Pinia store │
└─────────────────────────────────────────────────────────────────────────────────────┘
│
│ HTTP POST (Connect) to /a2a.v1.A2AService/*
│ Header: X-A2A-Agent-Headers: {"Authorization":"Bearer x"}
▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ BFF │
│ server.go: ExtractAgentHeaders(r) → WithAgentHeaders(ctx) → proxy handler │
│ Protocol switch: cfg.Protocol ? NewJSONRPCProxy : NewGrpcProxy │
└─────────────────────────────────────────────────────────────────────────────────────┘
│ │
┌───────────┴───────────┐ ┌────────────┴────────────┐
│ grpcProxy │ │ jsonrpcProxy │
│ metadata.Append(...) │ │ agentHeadersInterceptor │
│ → gRPC agent │ │ req.Meta.Append(...) │
│ localhost:8080 │ │ → HTTP JSON-RPC agent │
└───────────────────────┘ │ http://.../jsonrpc │
└─────────────────────────┘
How it works
1. Frontend
- User input flows through
PlaygroundViewinto the messages store (app/src/pages/playground/store/messages.ts), which builds aSendMessageRequestand callscreateA2AClient().sendStreamingMessage(request). - The Connect client (
app/src/clients/a2aClient.ts) uses same-origin requests, so all A2A calls go to the BFF (e.g.http://localhost:3000). - Before every request, the
agentHeadersInterceptorreads the Pinia store (app/src/store/agentHeaders.ts), serializes configured headers to JSON, and sets theX-A2A-Agent-HeadersHTTP header. Headers are configured in the UI (key icon) and can be persisted to localStorage.
2. BFF routing and headers
- The BFF (
internal/bff/server.go) mounts two handler groups: the A2A Connect proxy at/a2a.v1.A2AService/and the static SPA at/. - All A2A requests pass through middleware that reads
X-A2A-Agent-Headers, parses it asmap[string]string, and puts it into the request context (internal/bff/headers.go). The chosen proxy can then read these headers.
3. Protocol switching
- The CLI (
cmd/a2a-playground/main.go) sets--jsonrpcor defaults to gRPC. That value is passed intoServerConfig.Protocol. NewServercheckscfg.Protocol: if JSON-RPC, it createsNewJSONRPCProxy(cfg.AgentURL); otherwiseNewGrpcProxy(cfg.AgentURL). Both proxies implementA2AServiceHandler, so routing stays the same; only the outbound transport differs.
4. gRPC proxy
internal/bff/proxy.go: The gRPC proxy usesgo.alis.build/clientto connect to the agent. Before each call,withAgentHeaders(ctx)reads headers from context and adds them tometadata.NewOutgoingContext. Those metadata entries become gRPC headers on the agent call. The proxy forwards Connect requests as native gRPC calls.
5. JSON-RPC proxy
internal/bff/jsonrpc_proxy.go: The JSON-RPC proxy usesa2aclientfroma2a-gowithWithJSONRPCTransport. It converts Connect/protobuf requests to JSON-RPC usingpbconv.FromProto*andpbconv.ToProto*. The proxy is built withWithInterceptors(&agentHeadersInterceptor{}), which reads headers from context and appends them toreq.Meta; the a2a-go client sendsMetaas HTTP headers to the agent's JSON-RPC endpoint.ListTasksis not supported over JSON-RPC per the A2A spec and returnsCodeUnimplemented.
Development
Run in dev mode
Uses app/dist on disk instead of embedded files (no rebuild needed after frontend changes):
make run
This runs with --dev --agent-url=localhost:8080.
Generate protobuf only
make generate
Runs buf generate in packages/a2a (generates TS into app/packages/a2a/protobuf and Go into gen/go).
Frontend development
cd app && pnpm dev
Build frontend for production:
cd app && pnpm build
Releasing
The binary embeds app/dist at build time. For go run and go install with a version to work, the built frontend must be committed before tagging:
make generate build-frontend
git add app/dist
git commit -m "chore: embed frontend for v1.0.0"
git tag v1.0.0
git push origin main --tags
The release workflow then builds the cross-platform binaries and attaches them to the GitHub release.
Project structure
| Directory | Description |
|---|---|
cmd/a2a-playground/ |
CLI entrypoint |
internal/bff/ |
BFF server, static serving, Connect proxy (gRPC or JSON-RPC) |
packages/a2a/ |
Proto definitions and buf config (A2A canonical) |
app/ |
Vue 3 + Vuetify SPA |
gen/go/ |
Generated Connect handlers (uses a2a-go/a2apb) |
Contributing
- Fork and open a PR
- Use conventional commits
- Run tests:
go test ./...
License
See LICENSE.