Modern AI applications rarely communicate through only one interface. The same agent may need to support a direct chat experience, communicate with another agent, or expose capabilities through the Model Context Protocol (MCP).
Building a separate agent implementation for every channel creates unnecessary duplication.
A better architecture is to keep the agent's core behavior independent from its communication channels and place channel-specific adapters around it.
This approach allows a single .NET agent to serve different interaction patterns while keeping business logic, tools, security, and observability centralized.
Why Share One Agent?
Consider an enterprise support agent.
It may need to handle:
User Chat
|
v
Support Agent
Other Agent
|
v
Support Agent
MCP Client
|
v
Support AgentA poor implementation might create:
ChatAgent
A2AAgent
McpAgentEach implementation can eventually develop slightly different:
Prompts
Tool permissions
Validation
Business rules
Error handling
Telemetry
That creates behavioral drift.
A better architecture is:
Support Agent
|
+------------+------------+
| | |
v v v
Chat A2A MCP
Adapter Adapter AdapterThe agent remains the central capability while each adapter handles the protocol-specific communication.
Core Architecture
A clean .NET design separates four concerns:
Channel
↓
Adapter
↓
Agent Orchestrator
↓
Tools / Services
↓
Enterprise SystemsFor example:
Chat Request
↓
Chat Adapter
↓
Support Agent
↓
Customer Service
↓
DatabaseAn A2A request follows the same internal path:
Agent Request
↓
A2A Adapter
↓
Support Agent
↓
Customer ServiceThe communication protocol changes, but the business capability does not.
Define a Common Agent Contract
Start by defining an internal abstraction.
public interface IAgent
{
Task<AgentResponse> ExecuteAsync(
AgentRequest request,
CancellationToken cancellationToken);
}The request should contain normalized information rather than protocol-specific objects.
public sealed record AgentRequest(
string ConversationId,
string? UserId,
string Message,
IReadOnlyDictionary<string, string>? Metadata);The response can similarly remain protocol-neutral:
public sealed record AgentResponse(
string Message,
bool Success);Now the core agent does not need to know whether the request originated from chat, another agent, or an MCP-related workflow.
Implement the Agent
A simplified implementation could look like:
public sealed class SupportAgent : IAgent
{
private readonly ICustomerService customerService;
public SupportAgent(
ICustomerService customerService)
{
this.customerService = customerService;
}
public async Task<AgentResponse> ExecuteAsync(
AgentRequest request,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Message))
{
return new AgentResponse(
"A message is required.",
false);
}
// Agent orchestration would happen here.
return new AgentResponse(
"Request processed.",
true);
}
}The example intentionally keeps model-specific orchestration out of the interface.
That makes the abstraction easier to test.
The Chat Adapter
A chat endpoint can translate HTTP input into the common request.
[ApiController]
[Route("api/chat")]
public sealed class ChatController : ControllerBase
{
private readonly IAgent agent;
public ChatController(IAgent agent)
{
this.agent = agent;
}
[HttpPost]
public async Task<IActionResult> Post(
ChatRequest request,
CancellationToken cancellationToken)
{
var agentRequest = new AgentRequest(
request.ConversationId,
request.UserId,
request.Message,
null);
var response =
await agent.ExecuteAsync(
agentRequest,
cancellationToken);
return Ok(response);
}
}The controller performs protocol translation.
It does not implement the agent's business behavior.
Why the Adapter Pattern Helps
Without adapters, channel-specific code can spread throughout the application:
Agent
|
+---- HTTP logic
+---- A2A logic
+---- MCP logic
+---- Authentication
+---- Business rules
+---- Tool invocationThis quickly becomes difficult to maintain.
With adapters:
Core Agent
|
+------------+------------+
| | |
ChatAdapter A2AAdapter MCPAdapterThe core remains focused.
Agent-to-Agent Communication
Agent-to-Agent, or A2A, communication is useful when multiple specialized agents need to collaborate.
For example:
Customer Agent
|
v
Order Agent
|
v
Billing AgentThe customer-facing agent does not need to understand every billing operation itself.
Instead, it can delegate an appropriate task to another agent.
Conceptually:
User
↓
Customer Agent
↓
"Get refund status"
↓
Billing Agent
↓
Result
↓
Customer Agent
↓
UserThe exact A2A protocol implementation depends on the framework and version being used.
The architectural principle remains the same: normalize the task at the application boundary and keep the core capability independent from transport details.
Model A2A Requests Explicitly
Instead of passing arbitrary dictionaries throughout the application, define a task model.
public sealed record AgentTask(
string TaskId,
string TaskType,
string Input,
string? RequestedBy);An A2A adapter can convert an incoming message into this internal representation.
var task = new AgentTask(
Guid.NewGuid().ToString(),
"GetOrderStatus",
message,
requestingAgent);This gives the application a stable internal contract.
MCP and the Agent
MCP serves a different purpose from A2A.
A simplified relationship is:
Agent
|
v
MCP Client
|
v
MCP Server
|
+---- Tool
+---- ResourceAn MCP server can expose tools and resources that an agent can use.
For example:
Support Agent
|
+---- SearchCustomer
|
+---- GetOrder
|
+---- SearchKnowledgeBaseThe agent decides when an available tool is appropriate, while the application should still enforce authorization and business rules around sensitive operations.
Do Not Confuse A2A and MCP
These technologies solve different problems.
| Area | A2A | MCP |
|---|---|---|
| Primary purpose | Agent-to-agent communication | AI-to-tool/resource integration |
| Main participant | Another agent | Tool/resource server |
| Typical interaction | Delegate a task | Invoke a capability |
| Example | Billing Agent | Search Orders Tool |
| Focus | Agent collaboration | Tool and context access |
A useful mental model is:
A2A
Agent ↔ Agent
MCP
Agent ↔ Tools / ResourcesThey can be used together.
One Agent, Multiple Channels
The architecture can therefore become:
Support Agent
|
+---------------+---------------+
| | |
v v v
Chat A2A MCP
Interface Agent Tasks Tool Access
| | |
+---------------+---------------+
|
v
Shared Services
|
v
Data SystemsThis allows the same core capability to be reused.
Authentication Must Remain Channel-Aware
Although the agent can be shared, authentication should not be treated identically across every channel.
For example:
Chat
↓
User Authentication
↓
User Identity
A2A
↓
Agent Authentication
↓
Calling Agent Identity
MCP
↓
Client Authentication
↓
Caller IdentityThe adapter should establish the caller identity before passing a normalized request to the core application.
Never assume that because an internal agent is calling the service, the operation is automatically authorized.
Authorization Should Be Centralized
Avoid implementing authorization separately:
Chat Authorization
A2A Authorization
MCP AuthorizationInstead:
Channel
↓
Identity
↓
Authorization Service
↓
AgentFor example:
public interface IAuthorizationService
{
Task<bool> CanExecuteAsync(
string? userId,
string operation,
CancellationToken cancellationToken);
}Then sensitive operations can be protected consistently.
Protect Tool Calls
Suppose the agent has a tool:
public interface IOrderService
{
Task<Order?> GetOrderAsync(
string orderId,
CancellationToken cancellationToken);
}A read operation may have relatively low risk.
A cancellation operation is different:
public interface IOrderService
{
Task CancelOrderAsync(
string orderId,
CancellationToken cancellationToken);
}The application should validate:
User Identity
↓
Order Ownership
↓
Order Status
↓
Cancellation Policy
↓
ExecuteThe model should not be the final authority for these rules.
Keep Protocol Types Out of the Domain Layer
Avoid code like:
public Task HandleMcpRequest(
McpSpecificRequest request)
{
// Business logic
}inside your core domain service.
Instead:
MCP Request
↓
MCP Adapter
↓
AgentRequest
↓
Core AgentThis reduces coupling.
The same principle applies to A2A and HTTP-specific request models.
Dependency Injection
Register the shared agent once:
builder.Services.AddScoped<IAgent, SupportAgent>();Register supporting services:
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IAuthorizationService, AuthorizationService>();Channel adapters can then depend on the same abstractions.
This prevents separate agent instances from developing independent implementations of the same business capability.
Observability Across Channels
A shared agent should also share its telemetry model.
Every execution can include:
ExecutionId
Channel
Caller
Agent
Model
Tool
Duration
Success
FailureFor example:
activity?.SetTag(
"agent.channel",
channel);
activity?.SetTag(
"agent.name",
"support-agent");Now a trace can distinguish:
Channel = chatfrom:
Channel = a2awithout requiring separate monitoring systems.
Handling Channel-Specific Capabilities
Not every capability needs to be available through every channel.
For example:
Chat
├── Search Orders
└── Cancel Order
A2A
└── Search Orders
MCP
├── Search Orders
└── Get CustomerThe authorization layer can enforce capability restrictions.
This is safer than assuming that sharing one agent means sharing every operation.
Error Handling
Normalize internal errors before returning them through a channel.
For example:
public sealed record AgentError(
string Code,
string Message);The internal agent can produce:
Code = ORDER_NOT_FOUNDThe chat adapter can translate that into a user-friendly response.
The A2A adapter may return a structured task failure.
The important point is that the business condition remains consistent.
Common Mistakes
Creating a Separate Agent for Every Channel
This duplicates business logic and increases maintenance.
Putting Protocol Logic Inside the Agent
The core agent should not depend heavily on HTTP, A2A, or MCP-specific types.
Sharing Authentication Blindly
Different channels can represent different identities.
Letting the Model Decide Authorization
Critical permissions should be enforced by deterministic application code.
Exposing Every Tool Everywhere
Channel-specific capability restrictions are often necessary.
Mixing Transport and Business Logic
Adapters should translate requests, not implement core business rules.
Ignoring Observability
A shared agent needs to identify which channel initiated each execution.
Best Practices
Keep one core agent implementation where the business capability is shared.
Use adapters for channel-specific protocols.
Define protocol-neutral request and response models.
Centralize business rules.
Keep authorization deterministic.
Identify the caller for every channel.
Restrict tools according to channel and risk.
Use structured error models.
Propagate correlation and trace IDs.
Monitor model and tool operations independently.
Keep protocol-specific types outside the core domain.
Test every adapter independently.
Test the shared agent separately from transport concerns.
Treat state-changing operations more strictly than read-only operations.
Advantages and Disadvantages
Advantages
Reduces duplicated agent logic
Keeps business behavior consistent
Makes new channels easier to add
Centralizes authorization and observability
Simplifies testing of core agent behavior
Allows different protocols to share the same services
Disadvantages
Requires clear architectural boundaries
Channel adapters add implementation complexity
Authentication and authorization still require channel-specific handling
Different protocols may have different capabilities and lifecycle models
Poorly designed shared abstractions can become too generic
Troubleshooting
Chat Works but A2A Fails
Check the adapter's request mapping and caller identity.
MCP Tool Is Available but Cannot Execute
Check tool registration, authorization, input validation, and MCP transport configuration.
Different Channels Produce Different Business Results
Compare the normalized AgentRequest objects reaching the core agent.
Authorization Works for Chat but Not A2A
Verify that the A2A adapter establishes the calling agent's identity correctly.
Duplicate Tool Execution
Inspect retries and determine whether the tool operation is idempotent.
Traces Are Difficult to Correlate
Ensure every channel propagates a common execution or trace identifier.
Testing the Shared Agent
The core agent should be tested without requiring a specific channel.
[Fact]
public async Task AgentRejectsEmptyMessage()
{
var agent = CreateAgent();
var request = new AgentRequest(
"conversation-1",
"user-1",
string.Empty,
null);
var result =
await agent.ExecuteAsync(
request,
CancellationToken.None);
Assert.False(result.Success);
}Then test the adapters separately.
This produces a clear testing hierarchy:
Core Agent Tests
+
Chat Adapter Tests
+
A2A Adapter Tests
+
MCP Integration TestsPractical Architecture
A production-oriented .NET solution can use:
Clients
|
+--------------+--------------+
| | |
v v v
Chat A2A MCP
Adapter Adapter Adapter
| | |
+--------------+--------------+
|
v
Agent Service
|
+--------------+--------------+
| | |
v v v
Authorization Tool Layer Business Services
| | |
+--------------+--------------+
|
v
Data SystemsThis architecture keeps the communication protocols at the edges while keeping the agent and business capabilities reusable.
Conclusion
Sharing one .NET agent across chat, A2A, and MCP channels is primarily an architectural problem rather than a protocol problem.
The most maintainable approach is to keep the agent's core behavior independent from the communication mechanism. Chat, A2A, and MCP should act as adapters that translate channel-specific requests into a common internal representation.
At the same time, authentication must remain channel-aware, authorization should remain deterministic, and tools should be exposed according to their risk and intended audience.
The resulting design is straightforward:
Chat ──┐
A2A ───┼──> Adapters ──> Shared Agent ──> Services
MCP ───┘This architecture reduces duplication, improves consistency, and makes it easier to add additional interaction channels without rewriting the underlying agent.
For production systems, the most important principle is simple: share the capability, not the protocol-specific implementation.

Join the conversation! Your thoughts help the community grow.