A2A Protocol
Expose Multica agents to external A2A v1.0 (Agent2Agent) clients. Built on the official a2a-python SDK; one Bearer token per workspace; opt-in per agent.
A2A (Agent2Agent) Protocol v1.0 is an emerging standard for agent-to-agent communication: a JSON-RPC + REST + SSE surface that lets one agent system call another's agents using a uniform Bearer-authenticated API. Multica ships a sidecar that publishes any of your agents as A2A-addressable endpoints — no extra coding required.
Why we adopted A2A
| Before A2A | After A2A |
|---|---|
Multica agents were reachable only via the web/desktop UI or multica CLI. | Any A2A-aware client (LangChain, CrewAI, custom orchestrators, the official a2a-inspector) can talk to your agents. |
| Integrations meant writing one-off HTTP wrappers per consumer. | A single standard protocol; an Agent Card declares everything a consumer needs to know. |
| No machine-to-machine auth — only user JWTs and PATs scoped to a person. | Workspace-scoped Bearer tokens (mca_…) — independent of any user's session, revocable in the UI. |
| External traffic would have polluted the issue list. | A2A traffic uses chat-style tasks (source='a2a', no issue) — cleanly separated from team workflows. |
| No per-source quotas; a runaway external client could starve your team. | Per-token rate limit + per-workspace in-flight cap (50 concurrent A2A tasks). |
Version compatibility — v1.0 and v0.3 in parallel
The gateway accepts both v1.0 and v0.3 on the same endpoints. v1.0 callers get v1.0-shaped responses; v0.3 callers get v0.3-shaped responses. The SDK negotiates per-request based on:
A2A-Versionheader (1.0or0.3)- Method name style:
SendMessage(v1.0) vsmessage/send(v0.3)
Mismatched combinations (e.g. A2A-Version: 1.0 + method: "message/send") are rejected as self-contradicting. Either send both v1.0 or both v0.3.
Dual support exists because early A2A SDKs and browser clients (the JS ecosystem in particular) still ship the v0.3 method-name + kind-discriminator + lowercase-enum format. Cutting them off would break every existing browser integration on day one. For new code, prefer v1.0.
What's supported
A2A v1.0 covers three transports — Multica's sidecar serves the first two natively (gRPC requires the SDK's [grpc] extra and is not enabled by default):
| Transport | Status |
|---|---|
| JSON-RPC 2.0 | ✅ |
| HTTP+JSON / REST | ✅ |
| gRPC | ⏸️ off by default |
All three communication patterns from the spec work:
- Sync request/response —
SendMessageblocks (default) or returns immediately (returnImmediately: true). - Streaming —
SendStreamingMessageandSubscribeToTaskdeliverTaskStatusUpdateEvent/TaskArtifactUpdateEventover SSE. - Push notifications —
CreateTaskPushNotificationConfig/Get/List/Delete. The sidecar POSTsStreamResponsepayloads to your webhook URL on every task update.
Other v1.0 specifics implemented:
- TaskState uses
TASK_STATE_*enum values (no lowercase legacy). - Part is unified — discriminate by member presence (
text/url/raw/data), nokindfield. - AgentCard uses
supportedInterfaces[]withtenantper interface (set to your workspace id). - Errors use
google.rpc.Statusshape withdomain: "a2a-protocol.org". - Multi-tenancy —
tenantfield flows through the request envelope and the card. - HTTP+JSON drops the
/v1prefix per spec (e.g.POST /message:send, notPOST /v1/message:send).
What's intentionally not in v1:
- Agent Card signing (JWS over JCS) — optional in the spec; we'll add it when federation/marketplace use-cases demand it.
- gRPC — pluggable, ship later if asked for.
- OAuth 2.0 device-code flow — Bearer suffices for our deployment.
- Cross-process streaming bridge — the sidecar uses the SDK's in-memory task store, so SSE/push works within one sidecar process. A future enhancement will bridge Multica's realtime hub for multi-instance setups.
What flows from your agent into the published card
Every field you set on an agent in the LK has a defined home on the A2A AgentCard — or is intentionally omitted. The mapping is enforced by tests; if it ever silently drifts, CI catches it.
| Multica LK field | A2A AgentCard location | Notes |
|---|---|---|
| Agent name | name | Required, copied 1:1. |
| Agent description | description | Truncated to 500 chars per spec. Falls back to instructions when blank. |
| Agent instructions | description (fallback only) | Used as the description source when description is empty. Otherwise NOT exposed — it's an internal system prompt. |
| Avatar URL | iconUrl | Public URL passes straight through. |
| Workspace name | provider.organization | Tells external clients who runs this agent. |
| A2A toggle | is_a2a_exposed (gate) | Required true for the card to be served at all. 404 otherwise. |
| Skills (id, name, description) | skills[] | Every attached skill gets one AgentSkill entry with id/name/description and the default inputModes/outputModes/tags: []. |
| Workspace ID | supportedInterfaces[].tenant | A2A multi-tenancy field; routes external clients correctly. |
Deliberately NOT exposed (would leak internal data or has no spec home):
instructions(whendescriptionis set) — internal system prompt.model,runtime_id,runtime_mode— backend orchestration detail.custom_env,custom_args,mcp_config— agent-process secrets and CLI flags.max_concurrent_tasks— internal throttle, already enforced server-side.visibility— Multica's internal access control. A2A exposure is a separate flag.- Skill
content,config— the script body itself stays internal; only the public name+description are advertised.
To preview exactly what an external A2A client will see, open Settings → A2A → Agent Card preview and pick the agent.
Quick start
1. Expose an agent
Open the agent (Workspace → Agents → click an agent), find the property row labeled A2A in the right inspector, and toggle it to Exposed. The agent is now publishable; nothing leaks until step 3.
Local-runtime agents are reachable too, but only when their daemon is online. For external clients you usually want a cloud runtime so the agent is always available.
2. Mint a workspace token
Settings → A2A → Mint token. Give it a memorable name (inspector-test, langchain-prod, …) and click Mint. A modal shows the raw secret (mca_…) once — copy it now, you won't see it again. The hash is what we store; the prefix (mca_xxxxxxxx…) stays in the table for identification.
CLI alternative:
zagents a2a token create <workspace-id> --name <label>
zagents a2a token list <workspace-id>
zagents a2a token revoke <token-id> --workspace <workspace-id>3. Point a client at your agent
The base URL for any external client is:
https://<your-host>/agents/<agent-id>with Authorization: Bearer <mca_…> and A2A-Version: 1.0 headers. Self-hosted Mac mini deployment uses https://a2a.magomadov.dev as the public host.
a2a-inspector
The official inspector gives you a chat UI plus compliance checks. Run it locally and point at the sidecar:
git clone https://github.com/a2aproject/a2a-inspector
cd a2a-inspector && bash scripts/run.sh
# Open http://127.0.0.1:5001
# Base URL: https://a2a.magomadov.dev/agents/<agent-id>
# Headers: Authorization: Bearer mca_…, A2A-Version: 1.0Programmatic — Python SDK
from a2a.client import A2AClient
client = A2AClient(
base_url="https://a2a.magomadov.dev/agents/<agent-id>",
headers={"Authorization": "Bearer mca_…"},
)
task = await client.send_message(text="Summarize the latest PRs.")
print(task.status.state) # TASK_STATE_COMPLETEDProgrammatic — curl
curl -s -X POST https://a2a.magomadov.dev/agents/<agent-id>/message:send \
-H "Authorization: Bearer mca_…" \
-H "A2A-Version: 1.0" \
-H "Content-Type: application/json" \
-d '{
"message": {
"messageId": "'$(uuidgen)'",
"role": "ROLE_USER",
"parts": [{"text": "ping"}]
},
"configuration": {"returnImmediately": true}
}'Preview, audit, revoke — all from the UI
Settings → A2A has three sections:
- Tokens — list with prefix, last-used time, created date; mint button; revoke action with confirmation.
- Agent Card preview — agent picker → formatted JSON of the card external clients see at
/.well-known/agent-card.json. Useful for spot-checking your agent's published description. - Recent A2A activity — most recent in-flight + completed A2A tasks with agent name, status badge, timestamps. Reflects only
source='a2a'traffic; UI-driven tasks don't appear.
Quotas and limits
| Limit | Value | Notes |
|---|---|---|
| Per-token rate limit | 60 req/min, 300 burst | In-memory on the sidecar; per-token. |
| Per-workspace concurrent A2A tasks | 50 | Counted across all agents in the workspace. |
| Per-agent concurrent tasks | MaxConcurrentTasks (default 6) | Existing Multica throttle — applies to A2A traffic just like UI-driven traffic. |
| Per-(agent, contextId) | strictly serial | Two A2A messages with the same contextId queue behind each other; different contextIds run in parallel. |
Limit excess returns a google.rpc.Status with reason: "WORKSPACE_QUOTA_EXCEEDED" (HTTP 429) or RESOURCE_EXHAUSTED. Tasks that hit per-agent throttle wait in TASK_STATE_SUBMITTED — the client can subscribe via SSE / push or poll GetTask.
Integrating your own services
Want to call a Multica agent from LangChain, CrewAI, n8n, Claude Code, your own Python/Go service, or plain curl? See A2A Integrations — recipes for each.
Maintenance — testing every change
A2A is a published spec, not internal API — silent drift breaks every external client at once. Two non-negotiable rules:
- Every change that touches A2A code (sidecar, card builder,
handler, executor, types, SDK upgrade) ends with a clean run of
the official
a2a-inspectorvalidator returningvalidation_errors: NONE. The unit tests (Go + Python + Vitest) are necessary but not sufficient — only the inspector validates the full wire format against the spec's pydantic schema. - Every upstream sync (
sync-upstream.shor manualgit merge upstream/main) ends with the post-merge sanity script fromUPSTREAM_SYNC.md("Защита приватного A2A-слоя при merge upstream") to verify no A2A patches were dropped during the merge — and then again with the inspector compliance probe.