The objective of this project is to implement and understand a complete distributed-agent workflow where:
- A client sends a request to Agent A.
- Agent A (orchestrator) delegates the task to Agent B using A2A JSON-RPC.
- Agent B (tool agent) uses MCP to call:
- an MCP tool (
add) - an MCP resource (
greeting://{name})
- an MCP tool (
- Agent B returns a structured A2A response to Agent A.
- Agent A returns a final composed response to the client.
This exercise validates:
- A2A communication between independent agents.
- MCP capability access from an agent.
- End-to-end interoperability with JSON-RPC payloads and artifact-based responses.
This repo now runs a full chain: Client -> Agent A -> Agent B -> MCP Server -> Agent B -> Agent A -> Client.
If you send text like:
compute 12 30 and greet Shashank
the system should produce a response shaped like:
[AgentA:orchestrator] Delegated to AgentB. Result: [AgentB:tool-agent] MCP says: 12+30=42. Also: Hello, Shashank! (from MCP)
A2A (Agent-to-Agent) is an interaction model where agents communicate over explicit protocol messages rather than direct function calls.
In this project, A2A characteristics are:
- Protocol boundary: communication happens over HTTP using JSON-RPC
message/send. - Agent isolation: Agent A and Agent B are separate services with separate ports.
- Structured exchange: results are returned as A2A artifacts (
result.artifacts[].parts[]). - Delegation model: Agent A does orchestration, Agent B does execution.
Why this matters:
- You can swap Agent B implementation without changing Agent A business flow.
- You can scale specialized agents independently.
- You keep orchestration and execution concerns separated.
MCP (Model Context Protocol) standardizes how an agent accesses external capabilities.
In MCP, the key abstractions are:
- Tools: callable functions with typed parameters (
add(a, b)). - Resources: URI-addressable data (
greeting://{name}). - Client session + transport: the agent opens a connection (here streamable HTTP) and interacts through a session API.
In this project:
mcp_server.pyhosts MCP capabilities athttp://127.0.0.1:8000/mcp.- Agent B uses
ClientSession+streamable_http_clientto:- initialize session
- call tool
add - read resource
greeting://<name>
- Agent B transforms MCP results into an A2A text artifact.
Why this matters:
- A2A solves agent orchestration.
- MCP solves capability/tool integration.
- Together they provide a clean layered architecture.
run_demo.py (client)
|
| POST JSON-RPC message/send
v
Agent A (agent_a.py, port 8001)
|
| POST JSON-RPC message/send
v
Agent B (agent_b.py, port 8002)
|
| MCP client session (streamable HTTP)
v
MCP Server (mcp_server.py, port 8000, /mcp)
|
| tool/resource output
v
Agent B -> Agent A -> run_demo.py
run_demo.py: sends a JSON-RPC message to Agent A and prints final text artifact.agent_a.py: orchestrator; forwards user text to Agent B and returns composed output.agent_b.py: worker/tool-agent; parses request, calls MCP tool+resource, returns computed answer.mcp_server.py: MCP service with:- tool:
add(a, b) - resource:
greeting://{name}
- tool:
requirements.txt: dependency list.older/: reference/legacy files, not used by the root workflow.
- Python 3.10+ (3.11 recommended)
pip- Four terminals/tabs (MCP, Agent B, Agent A, client)
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txtWindows PowerShell:
py -m venv .venv
.venv\Scripts\Activate.ps1
pip install --upgrade pip
pip install -r requirements.txtStart services in this order.
source .venv/bin/activate
python mcp_server.pysource .venv/bin/activate
uvicorn agent_b:app --host 127.0.0.1 --port 8002 --reloadsource .venv/bin/activate
uvicorn agent_a:app --host 127.0.0.1 --port 8001 --reloadsource .venv/bin/activate
python run_demo.pyAGENT_A_URL(default:http://127.0.0.1:8001/a2a/orchestrator)DEMO_TEXT(default:compute 12 30 and greet Shashank)
Example:
AGENT_A_URL=http://127.0.0.1:8001/a2a/orchestrator \
DEMO_TEXT="compute 7 9 and greet Alex" \
python run_demo.pyAGENT_B_BASE_URL(default:http://127.0.0.1:8002)AGENT_B_RPC_PATH(default: empty)
When AGENT_B_RPC_PATH is empty, Agent A tries:
//a2a/a2a/tool-agent
It caches the first successful URL for subsequent calls (optimization).
MCP_URL(default:http://127.0.0.1:8000/mcp)
In the root project code, Agent Card base URLs are explicitly set in build_agent_card(...):
- Agent A card URL:
http://127.0.0.1:8001 - Agent B card URL:
http://127.0.0.1:8002
Important:
- Agent Card URL is the agent's base URL metadata.
- JSON-RPC request path for this demo is the compatibility route:
- Agent A RPC:
http://127.0.0.1:8001/a2a/orchestrator - Agent B RPC:
http://127.0.0.1:8002/a2a/tool-agent
- Agent A RPC:
How to discover runtime paths quickly:
# Inspect FastAPI OpenAPI for available paths
curl -s http://127.0.0.1:8001/openapi.json | jq '.paths | keys'
curl -s http://127.0.0.1:8002/openapi.json | jq '.paths | keys'If jq is not installed:
curl -s http://127.0.0.1:8001/openapi.json
curl -s http://127.0.0.1:8002/openapi.jsonrun_demo.pysends JSON-RPCmessage/sendto Agent A.- Agent A extracts user text and calls Agent B over A2A JSON-RPC.
- Agent B parses two numbers and a name from user text.
- Agent B opens MCP session via
streamable_http_client. - Agent B calls MCP tool
add(a, b). - Agent B reads MCP resource
greeting://{name}. - Agent B composes text result and returns A2A artifact.
- Agent A wraps the result and returns final artifact to client.
run_demo.pyprints the final text.
- Creates
FastMCP("DemoMCP", json_response=True). - Defines tool:
add(a: int, b: int) -> int
- Defines resource:
greeting://{name}returningHello, <name>! (from MCP).
- Runs MCP server with streamable HTTP transport when executed as main.
- Input parsing:
_extract_text_from_parts(...)handles both dict and model-style parts._extract_numbers_and_name(...)parses integers and greeting target.
- MCP execution:
_call_mcp_add_and_greet(...)initializes MCP session.- calls
addtool. - reads
greeting://nameresource.
- Response composition:
_build_reply(...)returns either:- MCP result text, or
- guidance text if inputs are missing.
- A2A surfaces:
ToolAgent.execute(...)for SDK request pipeline.- compatibility route
POST /a2a/{assistant_id}for direct JSON-RPC demo calls.
- Agent card:
- advertises
compute_greetskill and examples.
- advertises
- Delegation helpers:
_candidate_b_rpc_urls()builds route fallback list._extract_text_from_response()parses artifact text from Agent B response.
- Optimization:
self._resolved_b_urlcaches first successful B endpoint.
- Delegation call:
_call_agent_b(...)posts JSON-RPC payload to B.- retries candidate routes until success.
- returns first text artifact or raises detailed error.
- A2A surfaces:
OrchestratorAgent.execute(...)for SDK flow.- compatibility route
POST /a2a/{assistant_id}for demo client route.
- Agent card:
- documents Agent A as orchestrator that delegates to MCP-enabled Agent B.
- Builds JSON-RPC user message payload.
- Sends request to Agent A URL.
- Validates HTTP and JSON-RPC response.
- Extracts first text artifact and prints it.
Input:
compute 12 30 and greet Shashank
Possible output:
[AgentA:orchestrator] Delegated to AgentB. Result: [AgentB:tool-agent] MCP says: 12+30=42. Also: Hello, Shashank! (from MCP)
Input with missing numbers:
hello there
Possible output:
[AgentA:orchestrator] Delegated to AgentB. Result: [AgentB:tool-agent] I am MCP-enabled. Send two numbers and a name, e.g. 'compute 12 30 and greet Shashank'.
This section shows how to implement the same architecture in a production-style AWS setup.
Deploy these services as containers:
agent-aservice (public entry)agent-bservice (private internal service)mcp-serverservice (private internal service)
With:
- CI/CD pipeline from GitHub
- secure service-to-service communication
- centralized logs/metrics
- scalable runtime
- Networking
- One VPC across 2-3 AZs.
- Public subnets for ALB only.
- Private subnets for ECS tasks (
agent-a,agent-b,mcp-server).
- Compute
- Amazon ECS on Fargate (3 services, one task definition each).
- Ingress
- Application Load Balancer (ALB) -> target group ->
agent-a.
- Application Load Balancer (ALB) -> target group ->
- Internal discovery
- ECS Service Connect or AWS Cloud Map so
agent-acan callagent-b, andagent-bcan callmcp-server.
- ECS Service Connect or AWS Cloud Map so
- Container registry
- Amazon ECR repositories for each service image.
- Secrets/config
- AWS Secrets Manager + SSM Parameter Store for env config.
- Observability
- CloudWatch Logs for all containers.
- CloudWatch Alarms on error rate/latency.
- X-Ray/OpenTelemetry optional for traces.
agent-a- Container port:
8001 - Env:
AGENT_B_BASE_URL=http://agent-b.internal(example Service Connect DNS)AGENT_B_RPC_PATH=/a2a/tool-agent
- Container port:
agent-b- Container port:
8002 - Env:
MCP_URL=http://mcp-server.internal/mcp
- Container port:
mcp-server- Container port:
8000
- Container port:
- On push to
main:- run lint/tests
- build 3 Docker images
- push images to ECR with commit SHA tags
- Deploy:
- update ECS task definitions with new image tags
- trigger rolling deployment for each ECS service
- Post-deploy checks:
- health check
agent-aendpoint via ALB - smoke test JSON-RPC call (same shape as
run_demo.py)
- health check
- Deploy
mcp-serverfirst. - Deploy
agent-band verify it reaches MCP. - Deploy
agent-aand verify it reachesagent-b. - Run smoke test through ALB endpoint.
- TLS at ALB (ACM certificate).
- Keep
agent-bandmcp-serverprivate (no public ingress). - Restrict Security Groups:
- ALB -> Agent A only.
- Agent A -> Agent B only.
- Agent B -> MCP only.
- Use task IAM roles with least privilege.
- Store secrets in Secrets Manager, not in code or task definition plaintext.
- Structured JSON logs from all services.
- Metrics:
- request count
- p95/p99 latency
- 4xx/5xx rates
- Alarms:
- Agent A 5xx > threshold
- Agent B MCP failures > threshold
- Synthetic canary job every 5 minutes:
- send
compute 12 30 and greet Ops - assert response contains sum and greeting.
- send
- Client calls
https://agents.yourdomain.com/a2a/orchestrator. - ALB routes to
agent-aECS task. - Agent A forwards JSON-RPC to
http://agent-b.internal/a2a/tool-agent. - Agent B calls
http://mcp-server.internal/mcp:- tool
add(12, 30)->42 - resource
greeting://Ops->Hello, Ops! (from MCP)
- tool
- Agent B returns A2A artifact to Agent A.
- Agent A returns final response to client.
You can implement this in:
- AWS CDK (Python/TypeScript), or
- Terraform (AWS provider).
Recommended stacks/modules:
network(VPC/subnets/route tables)security(SG/IAM/KMS)platform(ECS cluster/ALB/Cloud Map/ECR)services(agent-a,agent-b,mcp-server)observability(log groups/alarms/dashboards)
- Activate virtualenv.
- Reinstall dependencies:
pip install -r requirements.txt- Ensure MCP server is running on
127.0.0.1:8000. - Verify
MCP_URLif using non-default port/path.
- Ensure Agent B is running on
127.0.0.1:8002. - Set explicit endpoint if required:
AGENT_B_BASE_URL=http://127.0.0.1:8002 AGENT_B_RPC_PATH=/a2a/tool-agent uvicorn agent_a:app --port 8001- Use JSON-RPC payload with method
message/send. - Call compatibility endpoints like
/a2a/orchestratorand/a2a/tool-agentfor the demo style.
Install:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtRun all:
# Terminal 1
python mcp_server.py
# Terminal 2
uvicorn agent_b:app --host 127.0.0.1 --port 8002 --reload
# Terminal 3
uvicorn agent_a:app --host 127.0.0.1 --port 8001 --reload
# Terminal 4
python run_demo.py