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.
Combining A2A and MCP
A2A and MCP can be used together.
For example:
Customer Agent
|
| A2A
v
Order Agent
|
| MCP
v
Order Management ToolsThe customer agent does not need direct access to every order-management tool.
Instead, it delegates an appropriate task to the order agent.
The order agent then uses its authorized MCP tools.
This can create a useful separation of responsibilities.
Sharing the Same Agent Across Channels
The complete architecture can look like:
Users
|
v
Telegram
|
v
Telegram Adapter
|
|
Other Agents ---> A2A Adapter
|
|
MCP Clients ----> MCP Integration
|
v
Shared Agent
|
+----------------+----------------+
| | |
v v v
Customer Order Support
Service Service ServiceThe important part is that the business capabilities remain centralized.
Dependency Injection
Register the shared agent through dependency injection:
builder.Services.AddScoped<
IAgentService,
CustomerSupportAgent>();Register the underlying services:
builder.Services.AddScoped<
ICustomerService,
CustomerService>();
builder.Services.AddScoped<
IOrderService,
OrderService>();Channel adapters can then depend on IAgentService.
This prevents each channel from creating its own implementation of the same business capability.
Channel-Specific Authorization
A shared agent does not mean every channel should have identical permissions.
For example:
| Operation | Telegram | A2A | MCP |
|---|---|---|---|
| Search order | Yes | Yes | Yes |
| View customer profile | Yes | Yes | Restricted |
| Update customer data | Conditional | Conditional | Restricted |
| Cancel order | Confirmation | Policy-based | Restricted |
| Issue refund | Human approval | Policy-based | Highly restricted |
The exact policy should be determined by the application's security requirements.
Human Confirmation for High-Risk Actions
A user might write:
Cancel my order.The agent can understand the request, but the application can require confirmation before performing the operation.
User Request
↓
Agent Understands Intent
↓
Authorization
↓
Confirmation Required
↓
User Confirms
↓
Cancel OrderThis is particularly useful for irreversible or financially significant operations.
Error Handling Across Channels
Different channels may require different presentation formats.
The internal agent can use structured errors:
public sealed record AgentError(
string Code,
string Message);For example:
ORDER_NOT_FOUND
ORDER_ALREADY_CANCELLED
NOT_AUTHORIZED
SERVICE_UNAVAILABLETelegram can turn these into a conversational response.
An A2A adapter can return structured task status.
An MCP integration can expose the appropriate tool error.
The underlying business condition remains the same.
Observability
Every execution should have a correlation ID.
var executionId = Guid.NewGuid();
logger.LogInformation(
"Agent execution started. " +
"ExecutionId={ExecutionId}, Channel={Channel}",
executionId,
request.Channel);Track:
Execution ID
Channel
Agent
Model
Tool Calls
Duration
Token Usage
Retries
Final StatusThis makes it possible to answer questions such as:
Did the Telegram request fail?
Did the A2A task invoke the expected tool?
Which MCP operation caused the timeout?Testing the Shared Agent
The core agent should be tested independently of the channel.
[Fact]
public async Task EmptyMessageIsRejected()
{
var agent = CreateAgent();
var request = new AgentRequest(
"conversation-1",
string.Empty,
"user-1",
"telegram");
var result =
await agent.ExecuteAsync(
request,
CancellationToken.None);
Assert.False(result.Success);
}Then test each adapter separately.
Core Agent Tests
+
Telegram Adapter Tests
+
A2A Integration Tests
+
MCP Tool TestsThis makes failures easier to isolate.
Testing Tool Permissions
For sensitive tools, verify that unauthorized callers cannot execute them.
[Fact]
public async Task UnauthorizedCallerCannotCancelOrder()
{
var allowed =
await authorizationService.IsAllowedAsync(
"untrusted-agent",
"CancelOrder",
CancellationToken.None);
Assert.False(allowed);
}Authorization tests should remain deterministic.
Common Mistakes
Building Separate Business Logic for Every Channel
This creates duplication and inconsistent behavior.
Treating A2A Calls as Trusted
An agent-to-agent request still needs authentication and authorization.
Confusing MCP With A2A
MCP primarily provides tools and resources, while A2A enables agent-to-agent interaction.
Allowing Every Channel to Access Every Tool
Tool exposure should follow least-privilege principles.
Putting Protocol Types in Core Business Logic
Keep Telegram, A2A, and MCP-specific models at integration boundaries.
Letting the Model Enforce Authorization
The application must enforce permissions independently.
Ignoring Conversation State
Human-facing channels often require persistent state for useful multi-turn interactions.
Best Practices
Keep the core agent channel-neutral.
Use adapters for Telegram and other user-facing channels.
Normalize A2A requests before passing them to the agent.
Use MCP for controlled tool and resource integration.
Authenticate every external caller.
Enforce authorization outside the model.
Classify tools by risk.
Require confirmation for appropriate high-risk operations.
Keep conversation state behind an abstraction.
Use structured errors.
Add correlation IDs and distributed tracing.
Test the agent independently from its channels.
Test authorization and tool permissions separately.
Avoid exposing sensitive tools through every integration.
Advantages and Disadvantages
Advantages
Reuses the same agent capability across multiple channels
Reduces duplicated business logic
Makes additional integrations easier to introduce
Centralizes authorization and observability
Allows specialized agents to collaborate
Separates tool integration from agent behavior
Disadvantages
Multiple protocols increase architectural complexity
Authentication differs between channel types
Conversation state requires additional design
Tool permissions need careful governance
Debugging distributed agent workflows can be more difficult
Protocol implementations can evolve independently
Troubleshooting
Telegram Requests Reach the Bot but Not the Agent
Check the Telegram adapter's message mapping, authentication, and dependency-injection configuration.
A2A Requests Are Rejected
Verify caller authentication, task mapping, authorization, and protocol configuration.
MCP Tools Are Not Available
Check MCP client/server configuration, tool registration, connectivity, and permissions.
The Same Request Behaves Differently by Channel
Compare the normalized AgentRequest generated by each adapter.
A Tool Executes More Than Once
Inspect retries, duplicate messages, and whether the operation is idempotent.
Conversation History Is Lost
Check the conversation-state implementation and ensure the same conversation identifier is being propagated consistently.
Production Architecture
A production-oriented implementation can use the following structure:
External Clients
|
+----------------+----------------+
| | |
v v v
Telegram A2A MCP
Adapter Adapter Client
| | |
+----------------+----------------+
|
v
Agent Runtime
|
+--------------+--------------+
| | |
v v v
Authorization Tools Conversation
| | State
| | |
+--------------+--------------+
|
v
Business Services
|
v
Data SystemsObservability should surround the entire workflow:
Agent Execution
|
+---- Logs
+---- Metrics
+---- Traces
+---- Tool Activity
+---- Error EventsThis makes the system easier to operate and troubleshoot as the number of channels grows.
Conclusion
A multi-channel AI architecture does not require a separate business agent for every protocol. A better approach is to build a shared agent capability and connect it to Telegram, A2A, and MCP through well-defined integration boundaries.
Telegram provides a human-facing communication channel. A2A enables agents to delegate and collaborate on tasks. MCP provides a standardized way to connect agents with tools and resources.
The architecture becomes:
Telegram ──┐
A2A ───────┼──> Adapters / Integration ──> Shared .NET Agent
MCP ───────┘ |
v
Business ServicesThe most important production considerations are authentication, authorization, tool governance, conversation state, observability, and failure handling.
By keeping those concerns separated, developers can add new channels without rewriting the core agent and can maintain consistent business behavior regardless of where an agent request originates.

Join the conversation! Your thoughts help the community grow.