A2A Playground
36Local CLI with Vue UI for testing A2A agents in your browser. Supports 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 |
Authentication and headers
Use the Playground sidebar (right side) to configure:
- Connection settings – Agent URL and transport (gRPC or JSON-RPC).
- Authentication – Custom headers (e.g.
Authorization,X-API-Key,X-Tenant-ID) sent with every request. These are persisted and forwarded by the BFF to the agent. - OAuth 2.0 – Client ID, Client Secret, Authorization URL, Token URL, and scope. Click Authorize Connection to sign in via a popup; the access token is then sent as
Authorization: Bearer <token>on all A2A requests. The BFF forwards the incomingAuthorizationheader to the agent.
OAuth presets – You can save the current OAuth config under a nickname and load it later from the Saved configs dropdown in the OAuth section. Presets are stored on disk in ~/.a2a-playground/oauth/<nickname>.json. Client secrets in these files are stored unencrypted; keep the directory permissions restrictive and do not commit the folder to version control. The folder includes a .gitignore so its contents are ignored by git. Nicknames may only contain letters, numbers, underscore, dot, and hyphen.
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: agentConfigInterceptor (X-A2A-Agent-URL, X-A2A-Agent-Protocol) │
│ agentOAuthBearerInterceptor (Authorization: Bearer <token>) │
│ agentHeadersInterceptor (X-A2A-Agent-Headers from Pinia store) │
└─────────────────────────────────────────────────────────────────────────────────────┘
│
│ HTTP POST (Connect) to /a2a.v1.A2AService/*
│ Headers: Authorization, X-A2A-Agent-Headers, etc.
▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 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 Connect client adds: (1) agent config (
X-A2A-Agent-URL,X-A2A-Agent-Protocol) fromagentConnectionstore; (2)Authorization: Bearer <token>fromagentOAuthstore when the user has completed OAuth; (3) custom headers asX-A2A-Agent-Headers(JSON) fromagentHeadersstore. These are configured in the Playground sidebar and (for headers) 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 builds the agent headers from the incoming request (
internal/bff/headers.go): it parsesX-A2A-Agent-Headers(JSON) and merges in the request’sAuthorizationheader so OAuth Bearer tokens are forwarded. The result is put into the request context; the chosen proxy sends these headers to the agent.
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.