AI agents are becoming part of applications that communicate through multiple channels. A single business agent may need to interact with users through Telegram, communicate with other agents through Agent-to-Agent (A2A) protocols, and access enterprise tools through the Model Context Protocol (MCP).
Building each integration independently can quickly create duplicated orchestration logic, authentication code, tool definitions, and error handling.
Microsoft Agent Framework provides a way to structure agent applications around reusable capabilities while allowing different communication channels to connect to the same underlying agent.
The key architectural principle is simple: keep the agent's business capability independent from the channel used to communicate with it.
Understanding Agent Channels
A channel is the mechanism through which an agent receives or sends information.
A simplified architecture looks like this:
AI Agent
|
+--------------+--------------+
| | |
v v v
Telegram A2A MCP
| | |
v v v
Users Other Agents Tools/ResourcesThese channels solve different problems.
| Channel | Primary Purpose |
|---|---|
| Telegram | Human-to-agent communication |
| A2A | Agent-to-agent communication |
| MCP | Agent-to-tool and resource integration |
They should therefore not be treated as interchangeable protocols.
Why Use a Shared Agent?
Imagine a customer-support agent that can:
Find customer information
Check order status
Explain order policies
Create support requests
Escalate complex cases
The same capability could be exposed through:
Telegram → Customer Support Agent
A2A → Customer Support Agent
MCP → Customer Support CapabilitiesWithout a shared architecture, developers may create:
TelegramSupportAgent
A2ASupportAgent
McpSupportAgentThis creates multiple implementations of the same business behavior.
A better design is:
Support Agent
|
+-------------+-------------+
| | |
v v v
Telegram A2A MCP
Adapter Adapter AdapterThe adapters handle communication while the agent remains responsible for the core workflow.
Create a Channel-Neutral Agent Contract
A .NET application can start with a simple abstraction:
public interface IAgentService
{
Task<AgentResponse> ExecuteAsync(
AgentRequest request,
CancellationToken cancellationToken);
}Define a common request:
public sealed record AgentRequest(
string ConversationId,
string Message,
string? UserId,
string Channel);And a common response:
public sealed record AgentResponse(
string Message,
bool Success);The Channel property can be useful for authorization, telemetry, and routing decisions.
The core agent should not need to understand Telegram message objects, A2A transport objects, or MCP-specific protocol structures.
Implement the Agent
A simplified service might look like this:
public sealed class CustomerSupportAgent
: IAgentService
{
private readonly ICustomerService customerService;
public CustomerSupportAgent(
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);
}
// Model orchestration and tool selection
// would happen here.
return new AgentResponse(
"Your request has been received.",
true);
}
}The implementation can later be connected to the specific Microsoft Agent Framework APIs used by the application.
The important architectural boundary is that the application-facing contract remains stable.
Connecting Telegram
Telegram is primarily a user-facing channel.
The flow is:
Telegram User
|
v
Telegram Bot
|
v
Telegram Adapter
|
v
AgentRequest
|
v
Customer Support AgentThe Telegram adapter should translate incoming messages into the application's common request model.
Conceptually:
public async Task<AgentResponse> HandleTelegramMessageAsync(
TelegramMessage message,
CancellationToken cancellationToken)
{
var request = new AgentRequest(
ConversationId: message.ChatId.ToString(),
Message: message.Text,
UserId: message.UserId?.ToString(),
Channel: "telegram");
return await agent.ExecuteAsync(
request,
cancellationToken);
}The Telegram-specific object stays at the integration boundary.
Managing Conversation State
Human-facing channels normally require conversation state.
A useful abstraction is:
public interface IConversationStore
{
Task<ConversationState?> GetAsync(
string conversationId,
CancellationToken cancellationToken);
Task SaveAsync(
ConversationState state,
CancellationToken cancellationToken);
}The state might contain:
public sealed record ConversationState(
string ConversationId,
string? UserId,
IReadOnlyList<string> Messages);For a simple development application, an in-memory store may be sufficient.
For a production application with multiple instances, conversation state generally needs a shared persistence mechanism.
Connecting A2A
A2A addresses a different problem.
Instead of a person sending a message to an agent, another agent can request work.
For example:
Customer Agent
|
| Request order information
v
Order Agent
|
v
Order Service
|
v
Result
|
v
Customer AgentThe receiving agent should translate the incoming A2A task into the same internal representation used by other channels.
public async Task<AgentResponse> HandleAgentTaskAsync(
AgentTask task,
CancellationToken cancellationToken)
{
var request = new AgentRequest(
ConversationId: task.TaskId,
Message: task.Input,
UserId: null,
Channel: "a2a");
return await agent.ExecuteAsync(
request,
cancellationToken);
}The exact A2A message format and transport depend on the implementation and framework version.
The internal application contract does not need to change simply because the external protocol does.
A2A Authentication
Agent-to-agent communication should not be treated as automatically trusted.
The receiving application should establish:
Calling Agent
|
v
Authentication
|
v
Identity
|
v
Authorization
|
v
Requested OperationFor example:
public interface IAgentAuthorizationService
{
Task<bool> IsAllowedAsync(
string caller,
string operation,
CancellationToken cancellationToken);
}Then:
var allowed =
await authorizationService.IsAllowedAsync(
callerAgent,
"GetOrder",
cancellationToken);
if (!allowed)
{
return new AgentResponse(
"The requested operation is not authorized.",
false);
}The model itself should not determine whether another agent has permission to perform an operation.
Connecting MCP
MCP has a different role from Telegram and A2A.
A simplified MCP relationship is:
Agent
|
v
MCP Client
|
v
MCP Server
|
+---- Tools
|
+---- ResourcesAn MCP server can expose capabilities that an agent can discover and use.
For example:
Customer Support Agent
|
+---- SearchCustomer
|
+---- GetOrder
|
+---- SearchKnowledgeThis allows tool integration to remain separate from the core agent implementation.
Tool Definitions
A tool should have a clear contract.
For example:
public interface IOrderTool
{
Task<Order?> GetOrderAsync(
string orderId,
CancellationToken cancellationToken);
}The tool implementation should validate its input and enforce application-level authorization.
public async Task<Order?> GetOrderAsync(
string orderId,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(orderId))
{
throw new ArgumentException(
"Order ID is required.",
nameof(orderId));
}
return await repository.GetByIdAsync(
orderId,
cancellationToken);
}The agent can decide that it needs order information, but the tool remains responsible for executing the actual operation safely.
MCP Read vs Write Operations
Not every tool should have the same risk level.
Consider:
SearchOrders
↓
Read-only
UpdateOrder
↓
State change
CancelOrder
↓
Destructive operationA production application should classify tools accordingly.
public enum ToolRisk
{
ReadOnly,
StateChanging,
Destructive,
Critical
}This classification can influence authorization and approval requirements.
Join the conversation! Your thoughts help the community grow.