A dummy a2a agent for testing.
Project description
dummy-a2a
A programmable A2A 1.0 test agent. Send it a command keyword, get spec-compliant behavior back.
Use it to test your A2A client, validate spec compliance, test extension plugins, or run portable contracts against any server.
Pinned to a2a-sdk==1.0.0a0. Covers 11/11 operations, all 8 task states, 3 content types, and full extension negotiation. We track the SDK and will update as new releases land.
What you can validate
| Goal | How |
|---|---|
| Validate your client | Point your client at the dummy server. Send commands (echo, fail, stream, ask, ext, ...) and assert your client handles each response shape, state transition, SSE stream, and error code correctly. |
| Validate your server | Run the 38 portable contracts against your server. Contracts are dogfooded against the dummy server in CI, so you know they're correct. |
| Validate your extensions | Test extension negotiation end-to-end: header negotiation, artifact tagging, required extension enforcement, and multi-extension activation. |
Table of Contents
- Install
- Quick Start -- get running in 30 seconds
- Standalone server (HTTP, HTTPS, Docker)
- As a library
- Pytest fixtures (HTTP + HTTPS)
- Commands -- 14 command keywords
- Extensions -- A2A 1.0 extension negotiation
- Contract Testing -- 38 portable compliance contracts
- Spec Coverage
- Development
- License
Install
pip install dummy-a2a
Or from source:
git clone https://github.com/agsuy/dummy-a2a && cd dummy-a2a
uv sync --dev
Quick Start
1. Standalone server
# HTTP
dummy-a2a --port 9000
# HTTPS
dummy-a2a --port 9443 --ssl-certfile cert.pem --ssl-keyfile key.pem
# Docker
docker run -p 9000:9000 ghcr.io/agsuy/dummy-a2a
Try it out:
# Agent card
curl http://localhost:9000/.well-known/agent-card.json
# → {"name": "Dummy A2A Test Agent", "skills": [...], "capabilities": {...}, ...}
# Send a message
curl -X POST http://localhost:9000/ -H 'Content-Type: application/json' -d '{
"jsonrpc": "2.0", "id": 1,
"method": "SendMessage",
"params": {"message": {"messageId": "1", "role": 1, "parts": [{"text": "echo hello"}]}}
}'
# → {"result": {"task": {"id": "...", "contextId": "...", "status": {"state": "TASK_STATE_COMPLETED"}, "artifacts": [{"parts": [{"text": "hello"}]}], "history": [...]}}, "id": 1, "jsonrpc": "2.0"}
# Trigger a failure
curl -X POST http://localhost:9000/ -H 'Content-Type: application/json' -d '{
"jsonrpc": "2.0", "id": 1,
"method": "SendMessage",
"params": {"message": {"messageId": "1", "role": 1, "parts": [{"text": "fail"}]}}
}'
# → {"result": {"task": {"id": "...", "status": {"state": "TASK_STATE_FAILED", "message": {"role": "ROLE_AGENT", "parts": [{"text": "Deliberate failure for testing purposes."}]}}, "history": [...]}}, "id": 1, "jsonrpc": "2.0"}
2. As a library
from dummy_a2a import DummyA2AServer
async with DummyA2AServer(port=0) as server:
print(server.url) # http://127.0.0.1:<random>
# query with any HTTP client, any language, any A2A SDK
# With HTTPS
async with DummyA2AServer(port=0, ssl_certfile="cert.pem", ssl_keyfile="key.pem") as server:
print(server.url) # https://127.0.0.1:<random>
3. Pytest fixtures
Drop this in your conftest.py:
from dummy_a2a.testing import a2a_server, a2a_url, a2a_http # noqa: F401
Write tests:
import pytest
@pytest.mark.asyncio
async def test_echo(a2a_url):
result = await my_a2a_client.send(a2a_url, "echo hello")
assert result.state == "TASK_STATE_COMPLETED"
@pytest.mark.asyncio
async def test_failure(a2a_url):
result = await my_a2a_client.send(a2a_url, "fail")
assert result.state == "TASK_STATE_FAILED"
For HTTPS testing:
from dummy_a2a.testing import a2a_https_server, a2a_https_url, a2a_https_http # noqa: F401
@pytest.mark.asyncio
async def test_tls(a2a_https_url):
assert a2a_https_url.startswith("https://")
# self-signed cert, auto-generated per test
All available fixtures
| Fixture | Type | Description |
|---|---|---|
a2a_server |
DummyA2AServer |
Server on random port |
a2a_url |
str |
http://127.0.0.1:<port> |
a2a_http |
httpx.AsyncClient |
Client with base_url set |
a2a_https_server |
DummyA2AServer |
TLS server (self-signed cert) |
a2a_https_url |
str |
https://127.0.0.1:<port> |
a2a_https_http |
httpx.AsyncClient |
TLS client (verify=False) |
webhook_receiver |
WebhookReceiver |
Collects push notifications |
Commands
Send a command keyword as the first word of your message:
| Command | Behavior | States |
|---|---|---|
echo <text> |
Echoes text back | completed |
stream <text> |
Streams response in chunks (SSE) | working, completed |
ask |
Asks for input, completes on follow-up | input_required, completed |
slow |
Runs ~10s with progress updates | working, completed/canceled |
fail |
Transitions to FAILED with error | failed |
reject |
Immediately rejects | rejected |
auth |
Requires auth token, completes on follow-up | auth_required, completed |
file |
Returns a FilePart artifact | completed |
data |
Returns a DataPart (JSON) artifact | completed |
multi |
Returns 3 artifacts with chunked delivery | completed |
ext |
Exercises extension negotiation | completed |
ext-required |
Enforces required extension or returns -32008 | completed/error |
debug |
Returns request metadata (extended card only) | completed |
<anything> |
Falls back to echo | completed |
Extensions
The dummy server implements A2A 1.0 extension negotiation for testing extension plugins.
How it works
Client Server
| |
| POST / + X-A2A-Extensions: urn:a2a:dummy:... |
| -------------------------------------------------> |
| | checks context.requested_extensions
| | activates matching extensions
| | tags artifacts with extension URIs
| Response + X-A2A-Extensions: urn:a2a:dummy:... |
| <------------------------------------------------- |
| |
- Agent card advertises extensions in
capabilities.extensions - Client sends
X-A2A-Extensionsheader with comma-separated URIs - Server activates recognized extensions, ignores unknown ones
- Response header echoes which extensions were activated
- Artifacts are tagged via
artifact.extensions
Registered extensions
| URI | Required | Params | What it does |
|---|---|---|---|
urn:a2a:dummy:echo-metadata |
no | none | Reflects negotiation state in response artifact |
urn:a2a:dummy:timestamp |
no | {"format": "iso8601"} |
Adds server timestamp to artifacts |
urn:a2a:dummy:required-test |
yes | none | Enforced by ext-required. Returns -32008 if missing |
Extension URIs are importable:
from dummy_a2a.agent_card import EXT_ECHO_METADATA, EXT_TIMESTAMP, EXT_REQUIRED
Testing extensions with curl
# Check what extensions the server supports
curl -s http://localhost:9000/.well-known/agent-card.json | jq '.capabilities.extensions'
# Negotiate extensions
curl -s http://localhost:9000/ \
-H 'Content-Type: application/json' \
-H 'X-A2A-Extensions: urn:a2a:dummy:echo-metadata, urn:a2a:dummy:timestamp' \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"1","role":1,"parts":[{"text":"ext"}]}}}' \
-D - 2>/dev/null | head -20
# Test required extension enforcement (returns -32008)
curl -s http://localhost:9000/ \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"1","role":1,"parts":[{"text":"ext-required"}]}}}'
# Satisfy the required extension
curl -s http://localhost:9000/ \
-H 'Content-Type: application/json' \
-H 'X-A2A-Extensions: urn:a2a:dummy:required-test' \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"1","role":1,"parts":[{"text":"ext-required"}]}}}'
Testing extensions with pytest
import httpx
import pytest
@pytest.mark.asyncio
async def test_extension_negotiation(a2a_url):
async with httpx.AsyncClient(base_url=a2a_url) as client:
resp = await client.post("/", json={
"jsonrpc": "2.0", "id": 1,
"method": "SendMessage",
"params": {"message": {"messageId": "1", "role": 1,
"parts": [{"text": "ext"}]}}
}, headers={"X-A2A-Extensions": "urn:a2a:dummy:echo-metadata"})
assert "urn:a2a:dummy:echo-metadata" in resp.headers["X-A2A-Extensions"]
task = resp.json()["result"]["task"]
assert "urn:a2a:dummy:echo-metadata" in task["artifacts"][0]["extensions"]
@pytest.mark.asyncio
async def test_required_extension_error(a2a_url):
async with httpx.AsyncClient(base_url=a2a_url) as client:
resp = await client.post("/", json={
"jsonrpc": "2.0", "id": 1,
"method": "SendMessage",
"params": {"message": {"messageId": "1", "role": 1,
"parts": [{"text": "ext-required"}]}}
})
assert resp.json()["error"]["code"] == -32008
Testing extensions with portable contracts
from dummy_a2a import verify_a2a_compliance
results = await verify_a2a_compliance(
"http://localhost:9000",
categories=["extensions"],
)
for r in results:
print(f"{'PASS' if r.passed else 'FAIL'} {r.contract_id}")
Contract Testing
38 portable contracts that verify A2A spec compliance against any server.
The dummy server is the reference implementation -- contracts are dogfooded against it in CI. Run them against your server to validate compliance.
Run contracts against your server
import asyncio
from dummy_a2a import verify_a2a_compliance
async def main():
results = await verify_a2a_compliance("http://localhost:9000")
for r in results:
print(f"{'PASS' if r.passed else 'FAIL'} {r.contract_id}: {r.detail}")
asyncio.run(main())
Run contracts as pytest
import pytest
from dummy_a2a.contracts import a2a_contracts
@pytest.mark.asyncio
@pytest.mark.parametrize("contract", a2a_contracts, ids=lambda c: c.id)
async def test_a2a_compliance(contract):
result = await contract.verify("http://localhost:9000")
assert result.passed, f"{result.contract_id}: {result.detail}"
Filter by category
results = await verify_a2a_compliance(
"http://localhost:9000",
categories=["agent-card", "streaming", "extensions"],
)
Categories: agent-card send-message task-state multi-turn get-task list-tasks cancel-task streaming content-types push-notifications errors extensions
All 38 contracts
| ID | Category | What it checks |
|---|---|---|
card.well-known |
agent-card | Card served at /.well-known/agent-card.json |
card.required-fields |
agent-card | Has name, description, version, skills, etc. |
card.skills-have-required-fields |
agent-card | Each skill has id, name, description, tags |
card.interface-protocol-version |
agent-card | Interface declares protocolVersion |
card.extended-card |
agent-card | Extended card via GetExtendedAgentCard |
send.completed |
send-message | SendMessage returns COMPLETED |
send.has-task-id |
send-message | Response has task ID and context ID |
send.has-artifacts |
send-message | Completed task has artifacts |
send.has-history |
send-message | Task includes message history |
state.failed |
task-state | FAILED state with error message |
state.rejected |
task-state | REJECTED state |
state.input-required |
task-state | INPUT_REQUIRED with prompt |
state.auth-required |
task-state | AUTH_REQUIRED with message |
multi-turn.input-required-follow-up |
multi-turn | Follow-up after INPUT_REQUIRED completes |
multi-turn.auth-required-follow-up |
multi-turn | Follow-up after AUTH_REQUIRED completes |
get-task.retrieves-task |
get-task | GetTask returns created task |
get-task.not-found |
get-task | GetTask errors on missing task |
get-task.includes-artifacts |
get-task | GetTask includes artifacts |
list-tasks.returns-tasks |
list-tasks | ListTasks returns created tasks |
cancel.cancels-task |
cancel-task | CancelTask transitions to CANCELED |
cancel.nonexistent-task |
cancel-task | CancelTask errors on missing task |
stream.sse-events |
streaming | SSE yields status + artifact events |
content.text-part |
content-types | TextPart artifact |
content.file-part |
content-types | FilePart with raw bytes |
content.data-part |
content-types | DataPart with structured JSON |
content.multi-artifact |
content-types | Multiple artifacts in one task |
push.create-config |
push-notifications | Create push notification config |
push.delete-config |
push-notifications | Delete push notification config |
error.method-not-found |
errors | Unknown method returns -32601 |
error.invalid-jsonrpc |
errors | Invalid jsonrpc version returns error |
ext.card-advertises-extensions |
extensions | Card has extensions with uri + description |
ext.negotiation-activates |
extensions | Request header activates, response header confirms |
ext.unknown-ignored |
extensions | Unknown extension URIs don't error |
ext.artifact-tagged |
extensions | artifact.extensions contains activated URIs |
ext.multiple-extensions |
extensions | Multiple extensions activated simultaneously |
ext.params-in-card |
extensions | Extension params accessible in card |
ext.required-enforced |
extensions | Missing required extension returns -32008 |
ext.required-satisfied |
extensions | Providing required extension succeeds |
Spec Coverage
| Area | Coverage |
|---|---|
| Operations | 11/11 -- SendMessage, SendStreamingMessage, GetTask, ListTasks, CancelTask, SubscribeToTask, push notification CRUD (4), GetExtendedAgentCard |
| Task states | All 8 -- submitted, working, input_required, completed, canceled, failed, rejected, auth_required |
| Content types | TextPart, FilePart (raw bytes), DataPart (structured JSON) |
| Extensions | 3 test extensions, header negotiation, artifact tagging, required enforcement (-32008), extension params |
| Agent card | Public card (12 skills, 3 extensions), extended card (adds debug), streaming + push + extensions capabilities |
Development
uv sync --dev
uv run pytest tests/ -v # 104 tests
uv run ruff check src/ tests/ # lint
uv run pyright # type check
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dummy_a2a-0.1.2.tar.gz.
File metadata
- Download URL: dummy_a2a-0.1.2.tar.gz
- Upload date:
- Size: 36.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.4 {"installer":{"name":"uv","version":"0.11.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3cde46cd7a13cb7bad1bdf2c3bfde609e3ee7a2a98931e1abc549805a8ca479a
|
|
| MD5 |
8b6aa9f1dff8bf15e80e96d74f7bb209
|
|
| BLAKE2b-256 |
e626351ef8981196b91f2fdc1388478d4ab9022e2ad87a78542451fc979b597c
|
File details
Details for the file dummy_a2a-0.1.2-py3-none-any.whl.
File metadata
- Download URL: dummy_a2a-0.1.2-py3-none-any.whl
- Upload date:
- Size: 35.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.4 {"installer":{"name":"uv","version":"0.11.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27e692f6fd719986ed4b0ffae577d7cfe0422d434240902984109cc75d7e111c
|
|
| MD5 |
2eef8db99334b40b205e05cd8a21765f
|
|
| BLAKE2b-256 |
80c075feab2013d957574b062638a5e6391aba3232dc7fb47c8692b3a539fea8
|