My journey into building scalable agentic systems began with a simple challenge: I needed a framework that could handle complex workflows with multiple AI agents working together seamlessly. Initially, I explored the Motia framework, which provided an excellent foundation for orchestrating event-driven workflows with zero infrastructure setup. The code-first approach and built-in observability were exactly what I needed for rapid development.
However, as my agent ecosystem grew more complex, I discovered the need for more dynamic context sharing between agents. This led me to the Model Context Protocol (MCP), which offered a standardized way to provide resources and prompt templates to AI models. I realized I could create a catalog of MCP resources that would automate the creation of specialized agents, each with their own unique capabilities but speaking a common language.
The final piece of the puzzle emerged when I thoroughly investigated the Agent-to-Agent (A2A) protocol. This discovery was transformative - it offered a standardized way for independent agents to discover and communicate with each other at scale. The decoupled nature of A2A meant agents could evolve independently while maintaining compatibility.
By combining these three powerful technologies - Motia's workflow orchestration, MCP's standardized context sharing, and A2A's discovery mechanisms - I created a comprehensive framework that could scale to hundreds of specialized agents organized into multiple flows. This Provider-Consumer pattern emerged as the architectural foundation of my approach, enabling independent agents to offer services, discover each other, and collaborate on complex tasks.
In this article, I'll walk you through this powerful pattern that has transformed how I build AI agent systems, allowing them to scale horizontally while maintaining manageability.
The Provider-Consumer pattern is a distributed architecture pattern where specialized agents expose capabilities (providers) that can be discovered and utilized by other agents (consumers) without direct coupling between them.
Key components in this pattern include:
- Provider Agents: Specialized in specific domains, exposing well-defined capabilities
- Consumer Agents: Searching for and utilizing provider capabilities
- Registry Service: Central discovery mechanism connecting consumers to relevant providers
- Communication Protocol: Standardized message format and exchange patterns
Let's examine a concrete implementation using our Capital Expert example.
Our provider agent specializes in knowledge about capital cities, now enhanced with MCP capabilities:
# Define agent skills
skills = [
{
"id": "capital_expert",
"name": "Capital Expert",
"description": "Provides information about capital cities of countries",
"examples": ["What is the capital of Germany?"]
}
]
# Define MCP capabilities
mcp_capabilities = {
"resources": True,
"prompts": True
}
# Define available MCP prompts
mcp_prompts = [
{
"name": "country_capital",
"description": "Get information about a country's capital",
"arguments": [
{
"name": "country",
"description": "The country to look up",
"required": True
}
]
}
]
# Define available MCP resources
mcp_resources = [
{
"uri": "capitals://database/all",
"name": "All Capital Cities",
"description": "Database of world capital cities",
"mimeType": "application/json"
}
]
# Create the agent with MCP capabilities
agent = create_a2a_mcp_agent(
agent_id="capital_expert_agent",
agent_name="Capital Expert",
agent_description="An agent that provides information about capital cities",
mcp_servers=["brave-search", "local-registry"],
model="gpt-4",
system_prompt="You are a geography expert specializing in capital cities...",
skills=skills,
mcp_capabilities=mcp_capabilities,
mcp_prompts=mcp_prompts,
mcp_resources=mcp_resources
)
# Register with the MCP registry
agent.start(auto_register=True)
The provider agent registers with the MCP registry, advertising its skills, MCP capabilities, available prompts, and resources. This enables discovery and provides a clear contract for consumers.
The consumer side demonstrates how to discover and interact with provider agents, including the use of MCP features:
# Import the discovery tools from the A2A-MCP framework
from a2a_mcp.client.agent_discovery import discover_agents
from a2a_mcp.client.a2a_client_agent import A2AClientAgent
# Discover a capital expert agent from the registry
async def main():
# Find an agent that has the capital_expert skill
agents = discover_agents(
skills=["capital_expert"],
registry_url="http://localhost:10001"
)
if not agents:
print("No capital expert agents found")
return
# Use the first matching agent
agent = agents[0]
print(f"Found agent: {agent.agent_name} ({agent.agent_id})")
# Agent capabilities are automatically detected by the framework
# Standard question
response = await agent.process_message("What is the capital of Germany?")
print_response(response)
# Using MCP prompt (automatically handled if supported)
response = await agent.process_message({
"type": "mcp_prompt",
"name": "country_capital",
"arguments": {"country": "France"}
})
print_response(response)
# Using MCP resource (automatically handled if supported)
response = await agent.process_message({
"type": "text_with_resources",
"text": "Compare the capitals of Spain and Portugal",
"resources": ["capitals://database/all"]
})
print_response(response)
# Framework handles all the details of checking capabilities and formatting responses
This example shows how the A2A-MCP framework handles the complexity of agent discovery and interaction, with the client code remaining simple and focused on the business logic.
The Model Context Protocol (MCP) extends the Provider-Consumer pattern with structured ways to share context and prompt templates:
Resources are structured data sources that providers can expose to consumers:
- Data Sharing: Providers can expose databases, files, or other data sources through a standardized interface
- URI-Based Access: Resources are addressed using URI schemes (e.g.,
capitals://database/all
) - Contextual Augmentation: Consumers can request resources alongside their queries to add context
Resources enable providers to share relevant data with consumers without embedding it in every response, making interactions more efficient.
Prompts are standardized templates that encode expert knowledge and interaction patterns:
- Template Definitions: Providers define prompt templates with well-specified arguments
- Argument Validation: Consumers fill in required arguments when using prompts
- Structured Interaction: Prompts guide conversations toward optimal outcomes
Prompts allow providers to encapsulate domain expertise in a standardized format, helping consumers follow best practices when interacting with specialized agents.
The Provider-Consumer pattern with MCP support enables composing complex workflows by orchestrating multiple specialized agents. Here are common composition patterns:
async def translate_capital_info(country_name, target_language):
# Find capital expert agent
capital_agent = await discover_agent_by_skill("capital_expert")
# Get the capital city using MCP prompt
capital_response = await capital_agent.process_message({
"type": "mcp_prompt",
"name": "country_capital",
"arguments": {"country": country_name}
})
capital_info = extract_text_from_response(capital_response)
# Find translation agent
translation_agent = await discover_agent_by_skill("translator")
# Translate the information
translation_response = await translation_agent.process_message(
f"Translate to {target_language}: {capital_info}"
)
return extract_text_from_response(translation_response)
async def get_country_info(country_name):
# Discover specialized agents
capital_agent = await discover_agent_by_skill("capital_expert")
population_agent = await discover_agent_by_skill("population_statistics")
# Process in parallel using MCP resources when available
tasks = [
capital_agent.process_message({
"type": "mcp_prompt",
"name": "country_capital",
"arguments": {"country": country_name}
}),
population_agent.process_message(f"What is the population of {country_name}?")
]
results = await asyncio.gather(*tasks)
# Aggregate results
return {
"capital": extract_text_from_response(results[0]),
"population": extract_text_from_response(results[1])
}
The A2A MCP framework provides comprehensive features for security, control, and monitoring, now enhanced with MCP-specific controls:
# Example of resource access configuration
resource_access = {
"capitals://database/all": {
"public": False,
"allowed_consumers": ["geography_app", "education_platform"],
"access_level": "read"
}
}
# Apply resource access control
await mcp_client.set_resource_access_controls(
agent_id="capital_expert_agent",
resource_access=resource_access
)
# Example of retrieving prompt usage metrics
prompt_metrics = await mcp_client.get_prompt_usage_metrics(
agent_id="capital_expert_agent",
prompt_name="country_capital",
time_range={
"start": "2025-01-01T00:00:00Z",
"end": "2025-01-31T23:59:59Z"
}
)
# Process and visualize metrics
for consumer, usage_count in prompt_metrics["usage_by_consumer"].items():
print(f"Consumer {consumer}: used {usage_count} times")
- Decoupling: Providers and consumers have no direct knowledge of each other
- Dynamic Discovery: Consumers find the right providers at runtime
- Specialization: Agents focus on specific domains for better performance
- Scalability: New providers can be added without modifying consumers
- Resilience: The system can recover from provider failures by discovering alternatives
- Structured Context: MCP resources provide standardized data sharing
- Expert Templates: MCP prompts encode domain expertise and best practices
- Composability: Complex workflows can be built by combining specialized agents
- Security: Comprehensive security model for production environments
- Observability: Detailed monitoring and logging for operational visibility
When implementing the Provider-Consumer pattern in A2A systems:
- Skill Definition: Carefully define provider skills with precise IDs, descriptions, and examples
- MCP Capability Design: Choose which MCP capabilities to expose based on agent expertise
- Resource Planning: Design resource URIs and schemas for maximum flexibility
- Prompt Templates: Create prompt templates that capture domain expertise
- Error Handling: Consumers should gracefully handle unavailable providers or capabilities
- Registry Reliability: Ensure the registry is reliable and available
- Authentication/Authorization: Consider which consumers can access which providers and resources
- Load Balancing: For multiple providers with the same skill, implement load balancing
- Security Boundaries: Clearly define trust boundaries and access controls
- Monitoring Strategy: Implement comprehensive monitoring for all critical components
The Provider-Consumer pattern in A2A MCP offers a powerful approach to building loosely coupled agent-based systems. By standardizing discovery, communication, and context sharing through MCP resources and prompts, it enables the creation of complex agent ecosystems where specialized capabilities can be easily composed into sophisticated workflows.
This architecture pattern works particularly well for AI agent systems where different models may excel at different tasks, allowing for a modular approach to building intelligent applications.
For developers looking to implement agent-based systems, this pattern provides a robust foundation for scalable, maintainable architectures that can evolve over time.