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 Agent

A poor implementation might create:

ChatAgent
A2AAgent
McpAgent

Each implementation can eventually develop slightly different:

That creates behavioral drift.

A better architecture is:

                  Support Agent
                       |
          +------------+------------+
          |            |            |
          v            v            v
        Chat          A2A          MCP
       Adapter       Adapter      Adapter

The 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 Systems

For example:

Chat Request
     ↓
Chat Adapter
     ↓
Support Agent
     ↓
Customer Service
     ↓
Database

An A2A request follows the same internal path:

Agent Request
     ↓
A2A Adapter
     ↓
Support Agent
     ↓
Customer Service

The 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 invocation

This quickly becomes difficult to maintain.

With adapters:

                 Core Agent
                     |
        +------------+------------+
        |            |            |
     ChatAdapter  A2AAdapter  MCPAdapter

The 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 Agent

The 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
 ↓
User

The 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
  +---- Resource

An MCP server can expose tools and resources that an agent can use.

For example:

Support Agent
     |
     +---- SearchCustomer
     |
     +---- GetOrder
     |
     +---- SearchKnowledgeBase

The 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.

AreaA2AMCP
Primary purposeAgent-to-agent communicationAI-to-tool/resource integration
Main participantAnother agentTool/resource server
Typical interactionDelegate a taskInvoke a capability
ExampleBilling AgentSearch Orders Tool
FocusAgent collaborationTool and context access

A useful mental model is:

A2A
Agent ↔ Agent

MCP
Agent ↔ Tools / Resources

They 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 Systems

This 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 Identity

The 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 Authorization

Instead:

Channel
   ↓
Identity
   ↓
Authorization Service
   ↓
Agent

For 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
      ↓
Execute

The 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 Agent

This 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
Failure

For example:

activity?.SetTag(
    "agent.channel",
    channel);

activity?.SetTag(
    "agent.name",
    "support-agent");

Now a trace can distinguish:

Channel = chat

from:

Channel = a2a

without 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 Customer

The 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_FOUND

The 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

  1. Keep one core agent implementation where the business capability is shared.

  2. Use adapters for channel-specific protocols.

  3. Define protocol-neutral request and response models.

  4. Centralize business rules.

  5. Keep authorization deterministic.

  6. Identify the caller for every channel.

  7. Restrict tools according to channel and risk.

  8. Use structured error models.

  9. Propagate correlation and trace IDs.

  10. Monitor model and tool operations independently.

  11. Keep protocol-specific types outside the core domain.

  12. Test every adapter independently.

  13. Test the shared agent separately from transport concerns.

  14. Treat state-changing operations more strictly than read-only operations.

Advantages and Disadvantages

Advantages

Disadvantages

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 Tests

Practical 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 Systems

This 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.