Skip to content

Repository files navigation

coding-agent-a2a

A2A + MCP server that wraps any supported coding-agent CLI (Cursor, Claude Code, Vibe, Codex, OpenCode) and exposes it to orchestrators over two protocols simultaneously on the same port:

ProtocolTransportWho uses it
A2A v1.0HTTP JSON-RPC 2.0 + SSEClaude Code, any A2A-compatible orchestrator
MCPstdio or HTTP (/mcp)Claude Desktop, any MCP host

Both protocols share the same underlying adapter, runner, and process-lifecycle logic.


Quick start

Run from source (local dev)

Prerequisites: Node.js 20+, one supported CLI on PATH.

git clone https://github.com/casabre/coding-agent-a2a
cd coding-agent-a2a
npm install
npm run build
cp .env.example .env # set AGENT_ADAPTER and AGENT_REPO_PATH at minimum
npm start

Run with Docker

Pull the base image and point it at your CLI binary:

docker run --rm \
-e AGENT_ADAPTER=claude-code \
-e CLAUDE_CODE_PATH=/usr/local/bin/claude \
-e AGENT_REPO_PATH=/workspace \
-v /path/to/my/project:/workspace \
-v /usr/local/bin/claude:/usr/local/bin/claude:ro \
-p 41242:41242 \
ghcr.io/casabre/coding-agent-a2a:latest

The base image contains only the server — no CLI binary. Mount or copy one in; see the Docker section for patterns.

Use with Claude Desktop

See docs/deployment.md for step-by-step Claude Desktop configuration, or jump to docs/claude-desktop-config.md.


Architecture overview

graph TB
subgraph Clients["External clients"]
MCPHost["MCP Host\n(Claude Desktop)"]
A2AClient["A2A Orchestrator\n(Claude Code)"]
end
subgraph Server["coding-agent-a2a :41242"]
Health["GET /health\n(no auth)"]
AgentCard["GET /.well-known/agent-card.json\n(no auth)"]
AuthMeta["OAuth metadata + proxy\n/.well-known/* · /authorize · /token\n(no auth — public PKCE endpoints)"]
AuthMW["Bearer auth middleware\n(when AUTH_ENABLED=true)"]
A2ARoute["POST /a2a/jsonrpc\nA2A layer"]
MCPRoute["stdio or /mcp\nMCP layer"]
Runner["ProcessRunner\nspawn + NDJSON parse"]
Adapters["Adapters\ncursor · claude-code · vibe · codex · opencode · generic"]
OTEL["OTEL SDK\ntraces + metrics → OTLP"]
end
IdP["Identity Provider\n(OIDC)"]
CLI["cursor-agent or claude or vibe or codex or opencode"]
Collector["OTEL Collector\n(Jaeger, Grafana, …)"]
MCPHost -->|"stdio / HTTP"| MCPRoute
A2AClient -->|"JSON-RPC + SSE"| A2ARoute
MCPHost -.->|"OAuth PKCE dance"| AuthMeta
AuthMeta -.->|"proxied"| IdP
A2ARoute --> AuthMW
MCPRoute --> AuthMW
AuthMW --> Runner
Runner --> Adapters
Adapters -->|spawn| CLI
Runner -.->|spans + metrics| OTEL
AuthMW -.->|spans| OTEL
OTEL -.->|OTLP| Collector
Loading

For a detailed component breakdown, design decisions, and event-flow diagrams, see docs/architecture.md.


Docker

Base image

The published image contains only the Node.js server — no CLI binary. This keeps it small and gives you full control over which agent binary runs inside the container.

ghcr.io/casabre/coding-agent-a2a:latest # latest main
ghcr.io/casabre/coding-agent-a2a:v0.1.0 # pinned release
ghcr.io/casabre/coding-agent-a2a:0.1 # minor-pinned

The server binary path is resolved at startup from the adapter-specific env var (e.g. CLAUDE_CODE_PATH) and falls back to the bare name on PATH.

Extending the base image

All example Dockerfiles live in examples/.

Copy in a pre-built binary

# examples/Dockerfile.claude-codeFROM ghcr.io/casabre/coding-agent-a2a:latest
USER root
COPY --from=my-claude-builder /usr/local/bin/claude /usr/local/bin/claude
RUN chmod +x /usr/local/bin/claude
USER node
ENV AGENT_ADAPTER=claude-code
docker build -f examples/Dockerfile.claude-code -t my-claude-agent .
docker run -e AGENT_REPO_PATH=/workspace -v /my/project:/workspace my-claude-agent

Add a language runtime (Python example)

The generic adapter can run any CLI that emits the expected NDJSON events. Add the runtime and your script on top of the base image:

# examples/Dockerfile.python-agentFROM ghcr.io/casabre/coding-agent-a2a:latest
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
COPY my_agent.py /usr/local/bin/my-agent.py
RUN chmod +x /usr/local/bin/my-agent.py
USER node
ENV AGENT_ADAPTER=generic \
AGENT_BINARY=python3 \
AGENT_ARGS=/usr/local/bin/my-agent.py

Your my_agent.py must produce NDJSON events on stdout. See docs/architecture.md for the event schema.

Mount the binary at runtime (no rebuild)

docker run --rm \
-e AGENT_ADAPTER=generic \
-e AGENT_BINARY=/bin/my-agent \
-v /usr/local/bin/my-agent:/bin/my-agent:ro \
-v /my/project:/workspace \
-e AGENT_REPO_PATH=/workspace \
-p 41242:41242 \
ghcr.io/casabre/coding-agent-a2a:latest

Health check

The image has a built-in HEALTHCHECK on /health. Docker reports the container healthy once the server has started:

docker inspect --format '{{.State.Health.Status}}' my-container
# healthy

Kubernetes (Helm)

A Helm chart ships with the repo under helm/coding-agent-a2a/.

Prerequisites

  • Helm 3
  • Kubernetes 1.21+

Install

helm install my-agent ./helm/coding-agent-a2a \
--set agent.adapter=claude-code \
--set image.tag=v0.1.0

Or from a values.yaml override file:

helm install my-agent ./helm/coding-agent-a2a -f my-values.yaml

Common configuration

ValueDefaultDescription
image.repositoryghcr.io/casabre/coding-agent-a2aImage to deploy
image.taglatestImage tag
agent.adaptercursorAdapter: cursor | claude-code | vibe | codex | opencode | generic
agent.timeoutMs120000Hard timeout per task (ms)
workspace.typeemptyDirWorkspace volume: emptyDir | pvc | hostPath
auth.enabledfalseEnable OAuth 2.0 Bearer auth
otel.enabledfalseEnable OpenTelemetry traces and metrics
otel.endpointOTLP receiver URL (e.g. http://otel-collector:4318)
replicaCount1Pod replicas (stateless — scales horizontally)

See helm/coding-agent-a2a/values.yaml for the full reference with inline comments.

Workspace volume

Each task spawns a CLI process in AGENT_REPO_PATH. Configure the workspace volume to match your use case:

# ephemeral (default — tasks work on files copied in by the caller)workspace:
type: emptyDir# persistent volume claim (provision new PVC automatically)workspace:
type: pvcpvc:
storageClass: standardsize: 20Gi# use an existing PVCworkspace:
type: pvcpvc:
claimName: my-existing-pvc# host path (dev/testing only)workspace:
type: hostPathhostPath:
path: /data/workspace

Auth in Kubernetes

Supply auth secrets separately from the chart values so they are not stored in plain text:

kubectl create secret generic my-agent-auth \
--from-literal=AUTH_ISSUER=https://idp.example.com \
--from-literal=AUTH_AUDIENCE=coding-agent \
--from-literal=AUTH_JWKS_URI=https://idp.example.com/.well-known/jwks.json
helm install my-agent ./helm/coding-agent-a2a \
--set auth.enabled=true \
--set auth.existingSecret=my-agent-auth \
--set auth.oidcDiscoveryUrl=https://idp.example.com/.well-known/openid-configuration

Enable OTEL in Kubernetes

# my-values.yamlotel:
enabled: trueendpoint: http://otel-collector.monitoring:4318serviceName: coding-agent-a2a

Environment variables

Core

VariableDefaultDescription
PORT41242HTTP port (A2A and, when MCP_TRANSPORT=http, MCP)
AGENT_ADAPTERcursorcursor | claude-code | vibe | codex | opencode | generic
AGENT_MODELModel override forwarded to the CLI
AGENT_TIMEOUT_MS120000Hard timeout per task (ms); 0 = disabled
AGENT_IDLE_EXIT_MS0Kill if no stdout for this long (ms); 0 = disabled
AGENT_FORCEtrueSkip shell-approval prompts (-f / --dangerously-skip-permissions)
AGENT_REPO_PATH.Working directory passed to the CLI as cwd
MCP_TRANSPORTstdiostdio (Claude Desktop spawns the process) or http
LOG_LEVELinfodebug | info | warn | error
CONFIG_FILEPath to a JSON config file; env vars always override file values

Adapter binary paths (optional overrides)

VariableDefaultAdapter
CURSOR_AGENT_PATHcursor-agentcursor
CLAUDE_CODE_PATHclaudeclaude-code
VIBE_BINARY_PATHvibevibe
CODEX_BINARY_PATHcodexcodex
OPENCODE_BINARY_PATHopencodeopencode
AGENT_BINARYgeneric (required)
AGENT_ARGS""generic — default arguments
AGENT_APPROVAL_PATTERNgeneric — regex to detect approval prompts
AGENT_APPROVAL_RESPONSEygeneric — response string for approval prompts

Authentication

All optional; only used when AUTH_ENABLED=true.

VariableDefaultDescription
AUTH_ENABLEDfalseEnable OAuth 2.0 Bearer auth on /a2a/jsonrpc and /mcp
AUTH_OIDC_DISCOVERY_URLOIDC discovery URL; auto-fills the three URLs below
AUTH_AUTHORIZATION_URLIdP /authorize endpoint (required if no discovery URL)
AUTH_TOKEN_URLIdP /token endpoint (required if no discovery URL)
AUTH_JWKS_URIIdP JWKS endpoint for token verification (required if no discovery URL)
AUTH_ISSUERExpected iss claim (required when AUTH_ENABLED=true)
AUTH_AUDIENCEExpected aud claim (required when AUTH_ENABLED=true)
AUTH_REQUIRED_SCOPESComma-separated scopes required on incoming tokens (e.g. agent:run)
AUTH_SERVER_URLhttp://localhost:PORTThis server as Authorization Server (OAuth metadata issuer URL)
AUTH_RESOURCE_URLAUTH_SERVER_URL/mcpThis server as Resource Server (RFC 9728)
AUTH_ALLOWED_REDIRECT_URISComma-separated allowed redirect URIs; if unset, IdP validates

Observability

VariableDefaultDescription
OTEL_ENABLEDfalseEnable OpenTelemetry traces and metrics
OTEL_EXPORTER_OTLP_ENDPOINTOTLP receiver URL (e.g. http://otel-collector:4318)
OTEL_SERVICE_NAMEunknown_serviceService name in traces and metrics
OTEL_TRACES_SAMPLERalways_onStandard OTEL sampling strategy
OTEL_METRIC_EXPORT_INTERVAL60000Metric push interval (ms)

Full configuration reference with validation rules: docs/deployment.md#configuration.


Authentication

By default (AUTH_ENABLED=false) all routes are open — suitable for local development and trusted-network deployments.

Set AUTH_ENABLED=true to require OAuth 2.0 Bearer tokens on:

  • POST /a2a/jsonrpc — A2A JSON-RPC surface
  • POST|GET|DELETE /mcp — MCP HTTP transport (when MCP_TRANSPORT=http)

The stdio MCP path is never affected by auth (the process is spawned directly by Claude Desktop — process-level trust).

The server mounts a full OAuth 2.0 proxy via the MCP SDK's ProxyOAuthServerProvider, so MCP clients (Claude Desktop, Claude Code) get standard /.well-known/oauth-authorization-server discovery and a complete Authorization Code + PKCE dance through to your IdP — without you implementing any OAuth logic.

Quickest setup — OIDC discovery URL

AUTH_ENABLED=true
AUTH_OIDC_DISCOVERY_URL=https://idp.example.com/.well-known/openid-configuration
AUTH_ISSUER=https://idp.example.com
AUTH_AUDIENCE=coding-agent

This auto-populates AUTH_AUTHORIZATION_URL, AUTH_TOKEN_URL, and AUTH_JWKS_URI from the IdP's discovery document at startup.

Manual URL setup (no discovery URL)

AUTH_ENABLED=true
AUTH_AUTHORIZATION_URL=https://idp.example.com/authorize
AUTH_TOKEN_URL=https://idp.example.com/token
AUTH_JWKS_URI=https://idp.example.com/.well-known/jwks.json
AUTH_ISSUER=https://idp.example.com
AUTH_AUDIENCE=coding-agent

OAuth endpoints exposed by this server

When AUTH_ENABLED=true, the following endpoints are mounted automatically (no auth required — these are public metadata/proxy endpoints):

EndpointDescription
GET /.well-known/oauth-authorization-serverOAuth server metadata (RFC 8414)
GET /.well-known/oauth-protected-resourceResource server metadata (RFC 9728)
GET /authorizeProxies to IdP authorize endpoint
POST /tokenProxies to IdP token endpoint

Production notes

  • AUTH_SERVER_URL must be HTTPS in production (the MCP SDK enforces this). localhost is whitelisted for development.
  • Restrict redirect URIs with AUTH_ALLOWED_REDIRECT_URIS for public deployments; if unset, redirect URI validation is delegated to the IdP.
  • CORS is set to * on all auth/discovery endpoints (intentional — these serve public PKCE-protected flows). Use a reverse proxy (nginx, Caddy) if you need restricted CORS.

Observability

coding-agent-a2a has built-in OpenTelemetry support for distributed tracing and metrics. It is off by default — set OTEL_ENABLED=true to activate it.

What is instrumented

Traces (spans)

SpanWhat it coversKey attributes
POST /a2a/jsonrpcFull HTTP request (auto-instrumented)http.method, http.route, http.status_code
POST /mcpFull HTTP request (auto-instrumented)http.method, http.route, http.status_code
cli.executeSubprocess lifetime: from spawn to process exitagent.adapter, agent.repo_path, agent.exit_code, agent.input_tokens, agent.output_tokens, agent.duration_ms
auth.verify_tokenJWT verification against JWKSauth.issuer, auth.success, auth.error

Metrics

MetricTypeLabelsDescription
agent.tokens.inputCounteradapterLLM input tokens consumed per task
agent.tokens.outputCounteradapterLLM output tokens generated per task
agent.task.duration_msHistogramadapterEnd-to-end CLI task duration in ms
agent.task.errorsCounteradapter, error_kindTask errors (spawn_error, nonzero_exit)

Token counts are reported by the CLI adapter in the done event. Not all adapters emit them; the counter stays at zero when unavailable.

Enable OTEL

OTEL_ENABLED=true \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
OTEL_SERVICE_NAME=coding-agent-a2a \
npm start

Any OTLP-compatible backend works: Grafana Tempo, Jaeger, OpenTelemetry Collector, Honeycomb, Datadog, etc.

Quick local setup with Docker Compose

# docker-compose.otel.ymlservices:
jaeger:
image: jaegertracing/all-in-one:latestports:
- "16686:16686"# Jaeger UI
- "4318:4318"# OTLP HTTP receiveragent:
image: ghcr.io/casabre/coding-agent-a2a:latestenvironment:
AGENT_ADAPTER: genericAGENT_BINARY: echoOTEL_ENABLED: "true"OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318OTEL_SERVICE_NAME: coding-agent-a2aports:
- "41242:41242"depends_on: [jaeger]
docker compose -f docker-compose.otel.yml up
# Open http://localhost:16686 to explore traces

Sampling

Set OTEL_TRACES_SAMPLER to control trace volume. See the OTEL SDK documentation for all options:

OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1 # sample 10% of traces

Using Different Adapters

Vibe CLI

export AGENT_ADAPTER=vibe
export VIBE_BINARY_PATH=/path/to/vibe # Optional, defaults to "vibe"export AGENT_REPO_PATH=/path/to/repo
npm start

Codex CLI

export AGENT_ADAPTER=codex
export CODEX_BINARY_PATH=/path/to/codex # Optional, defaults to "codex"export AGENT_REPO_PATH=/path/to/repo
npm start

OpenCode CLI

export AGENT_ADAPTER=opencode
export OPENCODE_BINARY_PATH=/path/to/opencode # Optional, defaults to "opencode"export AGENT_REPO_PATH=/path/to/repo
npm start

Custom CLI (Generic Adapter)

export AGENT_ADAPTER=generic
export AGENT_BINARY=/path/to/your-agent # Requiredexport AGENT_ARGS="--stream --model my-model"# Optional: default argsexport AGENT_APPROVAL_PATTERN="\[Y\/n\]"# Optional: custom approval patternexport AGENT_APPROVAL_RESPONSE="yes"# Optional: custom approval response
npm start

Note: The generic adapter assumes your CLI produces NDJSON-compatible streaming output. All built-in adapters (cursor, claude-code, vibe, codex, opencode) use the same NDJSON event parser.


MCP tools

ToolDescription
coding_agent_runSubmit a coding task; returns job_id immediately
coding_agent_pollPoll new events since a given line offset
coding_agent_resultRetrieve the final result and clean up the job
coding_agent_cancelCancel a running job
coding_agent_infoReturn adapter name, capabilities, and server version

Full parameter schemas and examples: docs/api/mcp-tools.md.


A2A protocol

The A2A surface exposes one skill (code-task) and supports streaming via message/stream.

EndpointAuthDescription
GET /healthnoneLiveness probe — returns {"status":"ok","adapter":"<name>"}
GET /.well-known/agent-card.jsonnoneA2A agent card (spec-required, always public)
POST /a2a/jsonrpcBearer (when AUTH_ENABLED=true)JSON-RPC 2.0 + SSE streaming

Full method reference, event shapes, and state machine: docs/api/a2a.md.


Development

See docs/development.md for:

  • Local dev setup
  • Test strategy (unit / integration / e2e)
  • How to add a new adapter

See CONTRIBUTING.md for the PR process and code standards.


Credits

Event-mapping patterns and protocol wiring inspired by:

About

A2A server wrapping the Cursor Agent CLI — delegate coding tasks to Cursor from Claude or any A2A-compatible orchestrator, with real-time streaming via the cursor-agent NDJSON event stream.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages