for Claude Code

Claude Code plugin · Agent2Agent protocol

Let Claude talk to other agents, and let them talk to Claude.

Call any A2A agent from a Claude Code session, or serve the session as one. JSON-RPC with SSE streaming, HTTP+JSON and gRPC, on both sides.

/plugin marketplace add fulgas/claude-a2a
/plugin install a2a@claude-a2a

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

ToolDoes
mcp__a2a__cardFetches an agent card from a base URL: the extended card when the saved credentials can sign in, and whether its signature verified
mcp__a2a__sendSends a message and streams progress until the task settles. contextId continues a conversation, taskId answers an input-required task
mcp__a2a__get_taskReturns a task’s state and artifacts
mcp__a2a__cancel_taskCancels 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
TransportEndpoint
Agent cardhttp://HOST:PORT/.well-known/agent-card.json
Card signing keyshttp://HOST:PORT/.well-known/jwks.json, with cardSigningKey
JSON-RPC + SSEhttp://HOST:PORT/a2a/jsonrpc
HTTP+JSONhttp://HOST:PORT/a2a/rest
gRPCHOST: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 }
    }
  }
}
OptionDefault
port41241HTTP port (card, JSON-RPC + SSE, HTTP+JSON)
grpcPort41242gRPC port
transportsallall, 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
host127.0.0.1Bind address. Anything else requires apiKey, oidcIssuer or tlsClientCa
publicHostthe bind addressThe 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
nameClaude CodeAgent name in the card
taskModeinlineWhere 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
apiKeyemptyAPI key callers may send in X-API-Key. Sensitive
oidcIssueremptyOpenID Connect or OAuth 2.0 issuer whose JWTs callers may send as Authorization: Bearer
oidcAudienceemptyThe aud those JWTs must carry. Required with oidcIssuer
requiredScopesemptyScopes those JWTs must all carry, separated by spaces
tlsCert, tlsKeyemptyPEM files. Both ports serve TLS
tlsClientCaemptyPEM file. Every connection must present a client certificate it signed (mutual TLS), as well as any other credential
cardSigningKeyemptyPrivate key file (PEM or JWK; EC, Ed25519 or RSA) that signs the agent card
maxConcurrentTasks4Incoming tasks that run at once; one more fails as busy. 0: no limit
tasksPerMinute30Tasks one caller may start in a minute. 0: no limit
authFailuresPerMinute20Failed sign-ins a client may make in a minute before it is refused (HTTP 429). 0: no limit
cardExtensionsemptyJSON 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
maxRequestMiB16MiB in one request, on every transport; a larger one is refused (HTTP 413, gRPC RESOURCE_EXHAUSTED)
outboxMaxFiles15Files a task may answer with from its outbox
outboxMaxFileMiB32MiB in one outbox file; sent in base64, about a third more on the wire
outboxMaxTotalMiB90MiB in a task’s outbox, all files together
autoServefalseStart the server when a session starts
clientApiKey$A2A_API_KEYAPI key sent to agents whose card asks for one. Sensitive
oauthClientId, oauthClientSecret$A2A_OAUTH_CLIENT_ID, $A2A_OAUTH_CLIENT_SECRETOAuth2 client for the client-credentials grant. The secret is sensitive
oauthScope, oauthAudience$A2A_OAUTH_SCOPE, $A2A_OAUTH_AUDIENCEScope and audience to ask the token endpoint for, besides the scopes the card names
oauthRedirectPort0Loopback 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_PASSWORDFor agents that ask for HTTP Basic. The password is sensitive
clientTlsCa$A2A_TLS_CAPEM file of the CA to trust instead of the system’s
clientTlsCert, clientTlsKey$A2A_TLS_CERT, $A2A_TLS_KEYPEM files of the client certificate, for agents that ask for mutual TLS
trustedCardKeys$A2A_CARD_JWKSKey set (URL or file) that agent card signatures must verify against
requireSignedCardsfalseRefuse 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-key metadata).
  • 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, exp and requiredScopes. The token’s sub names the caller in the task’s toast; a machine-to-machine token without one is named by its client (azp or client_id), and an exchanged one as sub 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 as joao via alva, or … (unverified), and the response’s A2A-Extensions echoes the extension; but only when clientId is the client the token names (azp or client_id) and sub is 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 Agent tool allowed, never in permissions.deny, when taskMode is subagent or fork: each task starts a subagent, and without the tool every task fails;
  • Read on the inbox and Edit on the outbox, as above, when its tasks send or get files;
  • CLAUDE_CONFIG_DIR set 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, unless CLAUDE_CONFIG_DIR puts 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 clientApiKey or $A2A_API_KEY
  • HTTP Bearer with $A2A_TOKEN, or HTTP Basic with clientUsername and clientPassword
  • OAuth 2.0 and OpenID Connect: $A2A_TOKEN as given, or a token from the client-credentials grant (with oauthClientSecret), the device-code flow, or the browser authorization-code flow with PKCE (for a public client, with only oauthClientId). The endpoints come from the card’s flow, its oauth2MetadataUrl or 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 clientTlsCert and clientTlsKey

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.

Edit this page on GitHub

Diagram

100%