Install
/plugin marketplace add fulgas/claude-a2a
/plugin install a2a@claude-a2a
This installs the latest release. To run main instead, while developing: just build, then claude --plugin-dir /path/to/claude-a2a.
Requires Node 20+ on PATH. Each release ships the bridge prebuilt in dist/, so there is no npm install step.
Client tools
| Tool | Does |
|---|---|
mcp__a2a__card | Fetches an agent card from a base URL: the extended card when the saved credentials can sign in, and whether its signature verified |
mcp__a2a__send | Sends a message and streams progress until the task settles. contextId continues a conversation, taskId answers an input-required task |
mcp__a2a__get_task | Returns a task’s state and artifacts |
mcp__a2a__cancel_task | Cancels a task |
Every tool takes transport (jsonrpc | rest | grpc; the default is the agent card’s preference). Credentials come from the saved client options or the environment (see Authentication); apiKey and token override them for one call, but anything passed that way lands in the transcript.
Server
/a2a-serve [--port 41241] [--grpc-port 41242] [--transports all|http|grpc] [--host 127.0.0.1] [--public-host NAME]
[--api-key K] [--oidc-issuer URL --oidc-audience AUD [--required-scopes "S1 S2"]]
[--tls-cert PEM --tls-key PEM [--tls-client-ca PEM]] [--card-signing-key PEM|JWK]
[--max-concurrent-tasks 4] [--tasks-per-minute 30] [--auth-failures-per-minute 20] [--name N]
[--task-mode inline|subagent|fork]
/a2a-stop
| Transport | Endpoint |
|---|---|
| Agent card | http://HOST:PORT/.well-known/agent-card.json |
| Card signing keys | http://HOST:PORT/.well-known/jwks.json, with cardSigningKey |
| JSON-RPC + SSE | http://HOST:PORT/a2a/jsonrpc |
| HTTP+JSON | http://HOST:PORT/a2a/rest |
| gRPC | HOST:GRPC_PORT |
With tlsCert and tlsKey, every one of them is served over TLS (https://); the card then names gRPC as https://HOST:GRPC_PORT.
Saved settings
Set defaults in /plugin, under the plugin’s options, or in settings.json:
{
"pluginConfigs": {
"a2a@claude-a2a": {
"options": { "port": 9300, "grpcPort": 9301, "host": "127.0.0.1", "name": "My Claude", "autoServe": true }
}
}
}
| Option | Default | |
|---|---|---|
port | 41241 | HTTP port (card, JSON-RPC + SSE, HTTP+JSON) |
grpcPort | 41242 | gRPC port |
transports | all | all, http (JSON-RPC + HTTP+JSON) or grpc. In grpc mode the HTTP port serves only the agent card, which clients need to find the gRPC endpoint |
host | 127.0.0.1 | Bind address. Anything else requires apiKey, oidcIssuer or tlsClientCa |
publicHost | the bind address | The host name the agent card gives callers, when they reach the server by another: host.docker.internal for a caller in Docker, with host 0.0.0.0 |
name | Claude Code | Agent name in the card |
taskMode | inline | Where a task runs: inline, a prompt in this conversation, one at a time; subagent, a subagent with a fresh context; fork, a subagent that starts with a copy of this conversation |
apiKey | empty | API key callers may send in X-API-Key. Sensitive |
oidcIssuer | empty | OpenID Connect or OAuth 2.0 issuer whose JWTs callers may send as Authorization: Bearer |
oidcAudience | empty | The aud those JWTs must carry. Required with oidcIssuer |
requiredScopes | empty | Scopes those JWTs must all carry, separated by spaces |
tlsCert, tlsKey | empty | PEM files. Both ports serve TLS |
tlsClientCa | empty | PEM file. Every connection must present a client certificate it signed (mutual TLS), as well as any other credential |
cardSigningKey | empty | Private key file (PEM or JWK; EC, Ed25519 or RSA) that signs the agent card |
maxConcurrentTasks | 4 | Incoming tasks that run at once; one more fails as busy. 0: no limit |
tasksPerMinute | 30 | Tasks one caller may start in a minute. 0: no limit |
authFailuresPerMinute | 20 | Failed sign-ins a client may make in a minute before it is refused (HTTP 429). 0: no limit |
cardExtensions | empty | JSON file holding an array of A2A extensions the card declares, each {uri, description?, required?, params?}. Checked at start; declared beside Secure Passport, each once, and signed with the card |
maxRequestMiB | 16 | MiB in one request, on every transport; a larger one is refused (HTTP 413, gRPC RESOURCE_EXHAUSTED) |
outboxMaxFiles | 15 | Files a task may answer with from its outbox |
outboxMaxFileMiB | 32 | MiB in one outbox file; sent in base64, about a third more on the wire |
outboxMaxTotalMiB | 90 | MiB in a task’s outbox, all files together |
autoServe | false | Start the server when a session starts |
clientApiKey | $A2A_API_KEY | API key sent to agents whose card asks for one. Sensitive |
oauthClientId, oauthClientSecret | $A2A_OAUTH_CLIENT_ID, $A2A_OAUTH_CLIENT_SECRET | OAuth2 client for the client-credentials grant. The secret is sensitive |
oauthScope, oauthAudience | $A2A_OAUTH_SCOPE, $A2A_OAUTH_AUDIENCE | Scope and audience to ask the token endpoint for, besides the scopes the card names |
oauthRedirectPort | 0 | Loopback port for the browser sign-in, for providers that need the exact redirect URI registered. 0: any free port |
clientUsername, clientPassword | $A2A_HTTP_USERNAME, $A2A_HTTP_PASSWORD | For agents that ask for HTTP Basic. The password is sensitive |
clientTlsCa | $A2A_TLS_CA | PEM file of the CA to trust instead of the system’s |
clientTlsCert, clientTlsKey | $A2A_TLS_CERT, $A2A_TLS_KEY | PEM files of the client certificate, for agents that ask for mutual TLS |
trustedCardKeys | $A2A_CARD_JWKS | Key set (URL or file) that agent card signatures must verify against |
requireSignedCards | false | Refuse agents whose card is not signed |
Sensitive options are kept in secure storage when set through /plugin.
Flags typed after /a2a-serve override the saved values for that run. Secrets reach the bridge through its environment, never on the command line.
Authentication
The plugin covers the security of the A2A specification: every security scheme, TLS, signed agent cards and the extended agent card. The only parts it leaves out are the implicit and password OAuth flows, which the specification deprecates.
Server. Incoming tasks run with this session’s tools and permissions, so the server binds to 127.0.0.1 by default and refuses any other host unless at least one of these is set. With an API key and an issuer both set, either one lets a caller in:
- API key: callers send
X-API-Key: <key>(gRPC:x-api-keymetadata). - OpenID Connect / OAuth 2.0 JWT: callers send
Authorization: Bearer <jwt>from your identity provider. At start the server reads the issuer’s discovery document (or its OAuth 2.0 metadata, RFC 8414) and key set, then checks each token’s signature (asymmetric algorithms only),iss,aud,expandrequiredScopes. The token’ssubnames the caller in the task’s toast; a machine-to-machine token without one is named by its client (azporclient_id), and an exchanged one assub via act.sub. - Relayed callers: a gateway in front of callers (such as an AG-UI server) calls with its own token, so the token names the gateway. With an issuer set, the card declares the Secure Passport v1 extension (optional, state key
onBehalfOf), and the gateway may name the caller it relays for in the message’s metadata under the extension’s URI:{"clientId": "alva", "state": {"onBehalfOf": {"sub": "joao", "verified": true}}}. The task is then shown asjoao via alva, or… (unverified), and the response’sA2A-Extensionsechoes the extension; but only whenclientIdis the client the token names (azporclient_id) andsubis a plain name, since it goes into the prompt. Otherwise the passport is ignored. It is the gateway’s word, not proof: limits and access stay on the token. - Mutual TLS: with
tlsClientCa, every connection must present a client certificate that CA signed. It goes with any other credential that is on; alone, the certificate’s CN names the caller.
The agent card advertises whichever schemes are on, and stays public, so clients can learn what to send. A refusal is a 401 with WWW-Authenticate challenges, or a 403 for a token without a required scope. With cardSigningKey, the card is signed, and its public key is served at /.well-known/jwks.json. A signed-in caller gets the extended agent card, which adds the limits it will meet.
Large requests. A text or data part over 64 KiB, and any part of bytes, does not enter the prompt: the bridge writes it to the task’s inbox, $TMPDIR/a2a-inbox-<random>/<random>/ (/tmp when TMPDIR is unset), as a read-only file, and the prompt names it in one line, such as [part 2: data, 9.0 MiB, in /tmp/a2a-inbox-…/part-2.json]. A data part’s file holds its JSON as sent. Claude reads it with its tools (a settings.json rule such as Read(//tmp/a2a-inbox-*/**) allows that). The inbox is removed once the task is answered.
Answering with files. Each task’s prompt names an outbox folder of its own, at $TMPDIR/a2a-outbox-<random>/<random>/ (/tmp when TMPDIR is unset): a settings.json rule such as Edit(//tmp/a2a-outbox-*/**) can allow writing there and nowhere else (Claude Code grants its file-writing tools, Write included, by Edit rules; a Write(...) rule is refused under dontAsk). Every file Claude writes directly in it goes back as an artifact named after the file without its extension: a .json file must hold one JSON object and is sent as data (pheme.plan.v1.json becomes the data artifact pheme.plan.v1), any other file as bytes with its media type (poster.jpg becomes poster, image/jpeg). The text answer follows, as the artifact answer. Links, folders, and more than outboxMaxFiles files (15), a file over outboxMaxFileMiB (32) or over outboxMaxTotalMiB in all (90) fail the task. The outbox is removed once the task is answered.
Running as a service. An agent that runs unattended, in a container for one, needs:
- the
Agenttool allowed, never inpermissions.deny, whentaskModeissubagentorfork: each task starts a subagent, and without the tool every task fails; Readon the inbox andEditon the outbox, as above, when its tasks send or get files;CLAUDE_CONFIG_DIRset to a folder that persists (/home/agent/.claude, say) when only that folder is kept: Claude Code keeps its first-run state in~/.claude.json, outside~/.claude, unlessCLAUDE_CONFIG_DIRputs it inside.
Where tasks run. With taskMode inline (the default), a task is a prompt in your conversation: you see it and its answer in the transcript, it waits until your own turn ends, and Claude answers it with everything the conversation holds. fork gives a subagent a copy of the conversation; subagent gives it none. With inline or fork, anyone allowed to send tasks can ask for what is in your conversation, pasted secrets included: use subagent when callers should not see it.
Limits. Each task runs Claude on your tokens, so the server caps them: maxConcurrentTasks at once, and tasksPerMinute per caller (an API key, a JWT subject, or anonymous). A task over a limit fails at once, before any subagent starts, with a status that says which. A client that keeps failing to sign in is refused for the rest of the minute after authFailuresPerMinute attempts, so a key cannot be guessed.
Client. The tools read the remote agent’s card, check its signature, and meet one of its security requirements, preferring one that needs no person. An agent whose card asks for nothing gets no credential.
- API key in the header, cookie or query parameter the card names, from
clientApiKeyor$A2A_API_KEY - HTTP Bearer with
$A2A_TOKEN, or HTTP Basic withclientUsernameandclientPassword - OAuth 2.0 and OpenID Connect:
$A2A_TOKENas given, or a token from the client-credentials grant (withoauthClientSecret), the device-code flow, or the browser authorization-code flow with PKCE (for a public client, with onlyoauthClientId). The endpoints come from the card’s flow, itsoauth2MetadataUrlor its OpenID Connect discovery document, and the scopes from the card’s requirement. A sign-in shows its URL and code as a notice and a toast, and the call waits for it. Tokens are kept in memory for the session and refreshed, so you sign in once. - Mutual TLS with
clientTlsCertandclientTlsKey
Without tlsCert, the server is plaintext: set it, or put a TLS proxy in front, before exposing the server beyond your machine, or a token can be read on the wire. gRPC over TLS needs a DNS name as the host, as Node refuses an IP address as the TLS server name.
How it works
The plugin’s hooks module can’t open sockets, so a Node bridge (dist/a2a.mjs, built on @a2a-js/sdk) speaks the wire protocols. Each incoming task runs where taskMode says: in your conversation, or in a subagent. docs/architecture.md has the full picture: arc42 sections, C4 diagrams and sequence diagrams.
Contributing
See CONTRIBUTING.md. Report vulnerabilities privately, as SECURITY.md says.
License
Licensed under either of Apache License, Version 2.0 or MIT license, at your option.