By Abdelrahman

Agent Mesh Interop: MCP for Tools + A2A for Agent-to-Agent (Without Building a Hairball)

ai-agentsmcpa2aagent-meshinteroperabilitydistributed-systemsmicroservicestool-connectivity

Teams are moving from one big agent to many small agents. That’s the trend. But it creates two problems: how do agents share tools, and how do they talk to each other?

The answer isn’t “just call tools directly” or “pass JSON between agents.” That breaks at scale. You need standards.

This article covers how to use MCP (Model Context Protocol) for tool connectivity and A2A (Agent-to-Agent) for agent communication. Together, they form an agent mesh that doesn’t become a hairball.

The actual problem

The N×M integration mess

You have 5 agents. Each agent needs access to 10 tools. That’s 50 integrations to build and maintain. Add 5 more agents. Now it’s 100 integrations. Add 10 more tools. Now it’s 200.

This is the N×M problem. Every agent talks to every tool. Every tool talks to every agent. The connections multiply.

Why “just call tools directly” breaks

At first, calling tools directly works. Agent A calls Tool X. Agent B calls Tool Y. Simple.

Then you need Agent A to also use Tool Y. You add the integration. Then Agent B needs Tool X. You add another integration. Then you need to version tools. Then you need to handle permissions. Then you need to audit who called what.

Before you know it, every agent has its own way of calling tools. Some use HTTP. Some use gRPC. Some use custom protocols. You can’t change a tool without breaking half your agents.

Ad-hoc JSON contracts don’t scale

Agents need to talk to each other. So you define a JSON format. Agent A sends a message to Agent B. It works.

Then Agent C needs to talk to Agent B. But Agent C has different needs. So you add optional fields. Then Agent D needs different fields. You add more optional fields. Then you need versioning. Then you need backward compatibility.

Six months later, you have 20 different message formats. Some are compatible. Some aren’t. Nobody knows which is which.

What changes with 5–20 agents

With 1–2 agents, you can get away with ad-hoc solutions. Direct tool calls work. Simple JSON messages work.

With 5–20 agents, you need:

  • Discovery: How do agents find each other? How do they find tools?
  • Delegation: How does Agent A ask Agent B to do something?
  • Policy: Who can call what? What are the rate limits? What’s the budget?
  • Coordination: How do agents avoid stepping on each other?

Without standards, you build a hairball. Every agent connects to every other agent. Every agent connects to every tool. The connections are unmanaged. The policies are scattered. The failures cascade.

Define your mesh: control plane vs data plane

An agent mesh has two planes: control and data. Keep them separate.

Control plane: identity, policy, discovery, routing

The control plane manages the mesh. It doesn’t handle messages or tool calls. It handles:

  • Identity: Who is Agent A? How do we authenticate it?
  • Policy: What can Agent A do? What tools can it access? What’s its rate limit?
  • Discovery: Where is Agent B? What can it do?
  • Routing: How do messages get from Agent A to Agent B?

Think of it like DNS or a service registry. It’s metadata about the mesh.

Data plane: messages, tool calls, results

The data plane handles actual work. It carries:

  • Messages between agents
  • Tool calls and responses
  • Results and errors

The data plane is fast. It doesn’t do policy checks. It doesn’t do discovery. It just moves data.

What an agent gateway does

An agent gateway sits between agents and the rest of the system. It handles:

  • Authentication: Is this request from a valid agent?
  • Authorization: Can this agent call this tool? Can it send messages to that agent?
  • Rate limiting: Has this agent exceeded its quota?
  • Budget tracking: How many tokens has this agent used?
  • Routing: Where should this message go?
  • Observability: Logging, tracing, metrics

The gateway is the single point of control. All traffic goes through it. All policies are enforced there.

Use MCP for tool connectivity

What MCP is

MCP (Model Context Protocol) is a standard for connecting agents to tools. It defines how tools expose their capabilities, how agents discover tools, and how tool calls work.

Think of it like REST for tools. Instead of every tool having its own API, tools implement MCP. Agents use MCP clients to call tools. The protocol handles discovery, versioning, and contracts.

Why tool servers matter

With MCP, tools run as servers. They expose their capabilities as a catalog. Agents query the catalog to see what’s available.

This solves several problems:

  • Tool catalogs: Agents can discover tools without hardcoding URLs
  • Permissions: Tools can declare what permissions they need
  • Isolation: Tools run in separate processes. A crash doesn’t take down agents
  • Versioning: Tools can expose multiple versions. Agents can choose which to use

Tool versioning and contract drift

Tools change. New versions add features. Old versions get deprecated. Without versioning, you break agents when you update a tool.

MCP handles this:

  • Tools expose version information
  • Agents can request specific versions
  • Tools can support multiple versions simultaneously
  • Breaking changes are explicit

You still need to manage contract drift. But MCP makes it visible. You can see which agents use which versions. You can plan migrations.

Use A2A for agent-to-agent

What A2A standardizes

A2A (Agent-to-Agent) is a protocol for agent communication. It defines:

  • Secure exchange: How agents authenticate and encrypt messages
  • Coordination: How agents delegate tasks and share results
  • Message formats: Standard envelopes and payloads

A2A doesn’t replace MCP. They work together. MCP is for tools. A2A is for agents.

When to use A2A messages vs tool calls

Use A2A messages when:

  • One agent needs another agent to do something
  • Agents need to coordinate (e.g., “wait for me to finish”)
  • Agents need to share context or state

Use tool calls (via MCP) when:

  • An agent needs to call an external service
  • An agent needs to access a database or API
  • The operation is stateless and doesn’t require agent coordination

The rule: agents talk to agents via A2A. Agents talk to tools via MCP.

A practical interop blueprint

Here’s how to build an agent mesh that doesn’t become a hairball.

Agent registry (service discovery)

Agents register themselves in a registry. They declare:

  • Their identity
  • Their capabilities (what they can do)
  • Their location (how to reach them)

Other agents query the registry to find agents. The registry is the control plane.

interface AgentRegistry {
  register(agent: AgentInfo): Promise<void>;
  discover(capability: string): Promise<AgentInfo[]>;
  get(agentId: string): Promise<AgentInfo | null>;
}

Message envelope

All messages use a standard envelope. It includes:

  • trace_id: For distributed tracing
  • tenant_id: For multi-tenancy
  • auth: Authentication tokens
  • budget: Token/time budget for this request
  • source_agent: Who sent this
  • target_agent: Who should receive this
interface MessageEnvelope {
  trace_id: string;
  tenant_id: string;
  auth: AuthToken;
  budget: {
    max_tokens?: number;
    max_time_ms?: number;
  };
  source_agent: string;
  target_agent: string;
  payload: unknown;
  timestamp: number;
}

Policy checks at the gateway

The gateway enforces policies before routing messages or tool calls:

  • RBAC/ABAC: Role-based or attribute-based access control
  • Data scopes: What data can this agent access?
  • Rate limits: How many requests per second?
  • Budget limits: How many tokens per hour?

If a request violates policy, the gateway rejects it. The agent never sees the tool or the other agent.

Timeouts, retries, and idempotency

Agent tasks need reliability:

  • Timeouts: How long should an agent wait for a response?
  • Retries: Should failed requests be retried?
  • Idempotency: Can this request be safely repeated?

These belong in the gateway and the message envelope. Not in individual agents.

Failure modes you will hit

Tool overload

Agents get access to many tools. They see all tool schemas in their context. With 50 tools, that’s a lot of context.

The solution: tool allowlists. Agents only see tools they’re allowed to use. The gateway filters the tool catalog based on policy.

Prompt injection through tool outputs

Tools return data. That data might contain prompts. If an agent uses tool output directly in a prompt, it’s vulnerable to injection.

The gateway should sanitize tool outputs. Strip control characters. Validate formats. Escape special sequences.

Infinite delegation loops

Agent A asks Agent B to do something. Agent B asks Agent C. Agent C asks Agent A. Loop.

Prevent this with:

  • Maximum delegation depth (e.g., 5 hops)
  • A seen-task hash set (track what’s been requested)
  • Timeouts on delegation chains

”Looks successful” but wrong

An agent completes a task. It returns success. But the result is wrong. Nobody verifies it.

Add a verification step. Another agent checks the result. Or the gateway validates against a schema. Or you use a consensus mechanism for critical operations.

Reference checklist

A2A/MCP fit: what they solve, what they don’t

MCP solves:

  • Tool discovery and versioning
  • Standard tool call protocol
  • Tool isolation and permissions

MCP doesn’t solve:

  • Agent-to-agent communication
  • Multi-agent coordination
  • Policy enforcement

A2A solves:

  • Agent-to-agent messaging
  • Secure agent communication
  • Task delegation between agents

A2A doesn’t solve:

  • Tool connectivity
  • Tool versioning
  • Tool permissions

What you still need to implement

A2A and MCP are protocols. You still need to build:

  • Auth: How do agents authenticate? How do you issue tokens?
  • Quotas: How do you track and enforce rate limits?
  • Audit logs: Who called what, when, and why?
  • Tracing: How do you trace a request across agents and tools?

These are implementation details. The protocols don’t define them. You choose how to implement them.

Code samples

Sample 1: Message envelope schema

All agents use this envelope format:

// src/types.ts
export interface MessageEnvelope {
  trace_id: string;
  tenant_id: string;
  auth: {
    agent_id: string;
    token: string;
    expires_at: number;
  };
  budget: {
    max_tokens?: number;
    max_time_ms?: number;
    spent_tokens?: number;
    spent_time_ms?: number;
  };
  source_agent: string;
  target_agent: string;
  payload: unknown;
  timestamp: number;
  delegation_depth: number;
}

export interface AgentMessage {
  type: 'request' | 'response' | 'error';
  task_id: string;
  action: string;
  parameters: Record<string, unknown>;
  context?: Record<string, unknown>;
}

Sample 2: Gateway middleware

The gateway enforces policies:

// src/gateway/middleware.ts
import { MessageEnvelope, AgentMessage } from '../types';

export class GatewayMiddleware {
  constructor(
    private agentRegistry: AgentRegistry,
    private policyEngine: PolicyEngine,
    private budgetTracker: BudgetTracker
  ) {}

  async processRequest(envelope: MessageEnvelope): Promise<MessageEnvelope> {
    // 1. Validate trace_id propagation
    if (!envelope.trace_id) {
      envelope.trace_id = this.generateTraceId();
    }

    // 2. Check per-agent tool allowlist
    const agentInfo = await this.agentRegistry.get(envelope.auth.agent_id);
    if (!agentInfo) {
      throw new Error('Agent not found');
    }

    const message = envelope.payload as AgentMessage;
    if (message.type === 'request' && message.action.startsWith('tool:')) {
      const toolName = message.action.replace('tool:', '');
      if (!agentInfo.allowed_tools.includes(toolName)) {
        throw new Error(`Tool ${toolName} not allowed for agent ${envelope.auth.agent_id}`);
      }
    }

    // 3. Check per-request token/time budget
    if (envelope.budget.max_tokens) {
      const usage = await this.budgetTracker.getUsage(envelope.auth.agent_id);
      if (usage.tokens_used >= envelope.budget.max_tokens) {
        throw new Error('Token budget exceeded');
      }
    }

    if (envelope.budget.max_time_ms) {
      const startTime = Date.now();
      envelope.budget.spent_time_ms = startTime - envelope.timestamp;
      if (envelope.budget.spent_time_ms > envelope.budget.max_time_ms) {
        throw new Error('Time budget exceeded');
      }
    }

    // 4. Check delegation depth
    if (envelope.delegation_depth > 5) {
      throw new Error('Maximum delegation depth exceeded');
    }

    return envelope;
  }

  private generateTraceId(): string {
    return `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
}

Sample 3: MCP tool server stub

A simple MCP tool server:

// src/mcp/tool-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

export class MCPToolServer {
  private server: Server;
  private tools: Map<string, ToolDefinition> = new Map();

  constructor() {
    this.server = new Server({
      name: 'example-tool-server',
      version: '1.0.0',
    }, {
      capabilities: {
        tools: {},
      },
    });

    this.setupHandlers();
  }

  private setupHandlers() {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: Array.from(this.tools.values()),
      };
    });

    // Call a tool
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const tool = this.tools.get(request.params.name);
      if (!tool) {
        throw new Error(`Tool ${request.params.name} not found`);
      }

      // Execute the tool
      const result = await this.executeTool(tool, request.params.arguments);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result),
          },
        ],
      };
    });
  }

  registerTool(tool: ToolDefinition) {
    this.tools.set(tool.name, tool);
  }

  private async executeTool(tool: ToolDefinition, args: unknown): Promise<unknown> {
    // Tool execution logic
    return tool.handler(args);
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
  }
}

// Example tool
const calculatorTool: ToolDefinition = {
  name: 'calculator',
  description: 'Performs basic arithmetic operations',
  inputSchema: {
    type: 'object',
    properties: {
      operation: { type: 'string', enum: ['add', 'subtract', 'multiply', 'divide'] },
      a: { type: 'number' },
      b: { type: 'number' },
    },
    required: ['operation', 'a', 'b'],
  },
  handler: async (args: any) => {
    const { operation, a, b } = args;
    switch (operation) {
      case 'add': return a + b;
      case 'subtract': return a - b;
      case 'multiply': return a * b;
      case 'divide': return b !== 0 ? a / b : { error: 'Division by zero' };
      default: throw new Error(`Unknown operation: ${operation}`);
    }
  },
};

MCP client call:

// src/mcp/tool-client.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

export class MCPToolClient {
  private client: Client;

  constructor(transport: Transport) {
    this.client = new Client({
      name: 'agent-client',
      version: '1.0.0',
    }, {
      capabilities: {},
    });
    this.client.connect(transport);
  }

  async listTools(): Promise<ToolDefinition[]> {
    const response = await this.client.request({
      method: 'tools/list',
    }, ListToolsRequestSchema);
    return response.tools;
  }

  async callTool(name: string, args: unknown): Promise<unknown> {
    const response = await this.client.request({
      method: 'tools/call',
      params: {
        name,
        arguments: args,
      },
    }, CallToolRequestSchema);
    return JSON.parse(response.content[0].text);
  }
}

Sample 4: A2A message handler

An A2A-style agent message router:

// src/a2a/message-handler.ts
import { MessageEnvelope, AgentMessage } from '../types';

export class A2AMessageHandler {
  constructor(
    private agentRegistry: AgentRegistry,
    private gateway: GatewayMiddleware
  ) {}

  async handleMessage(envelope: MessageEnvelope): Promise<MessageEnvelope> {
    // Process through gateway
    const processed = await this.gateway.processRequest(envelope);

    // Route to target agent
    const targetAgent = await this.agentRegistry.get(processed.target_agent);
    if (!targetAgent) {
      throw new Error(`Target agent ${processed.target_agent} not found`);
    }

    // Dispatch to agent
    const message = processed.payload as AgentMessage;
    const response = await this.dispatchToAgent(targetAgent, message, processed);

    // Create response envelope
    return {
      ...processed,
      source_agent: processed.target_agent,
      target_agent: processed.source_agent,
      payload: response,
      timestamp: Date.now(),
    };
  }

  private async dispatchToAgent(
    agent: AgentInfo,
    message: AgentMessage,
    envelope: MessageEnvelope
  ): Promise<AgentMessage> {
    // Call agent's handler
    const handler = this.getAgentHandler(agent);
    return await handler(message, envelope);
  }

  private getAgentHandler(agent: AgentInfo): (msg: AgentMessage, env: MessageEnvelope) => Promise<AgentMessage> {
    // Return agent's message handler
    // This would typically be loaded from the agent's implementation
    return async (msg, env) => {
      // Agent-specific logic
      return {
        type: 'response',
        task_id: msg.task_id,
        action: msg.action,
        parameters: { result: 'success' },
      };
    };
  }
}

Sample 5: Loop guard

Prevent infinite delegation loops:

// src/gateway/loop-guard.ts
import { MessageEnvelope } from '../types';

export class LoopGuard {
  private seenTasks: Map<string, Set<string>> = new Map();
  private readonly MAX_DEPTH = 5;
  private readonly TTL_MS = 60000; // 1 minute

  checkLoop(envelope: MessageEnvelope): void {
    // Check delegation depth
    if (envelope.delegation_depth > this.MAX_DEPTH) {
      throw new Error(`Maximum delegation depth ${this.MAX_DEPTH} exceeded`);
    }

    // Check for circular delegation
    const taskKey = this.getTaskKey(envelope);
    const seenSet = this.seenTasks.get(taskKey) || new Set();
    
    const delegationPath = `${envelope.source_agent}->${envelope.target_agent}`;
    if (seenSet.has(delegationPath)) {
      throw new Error(`Circular delegation detected: ${delegationPath}`);
    }

    // Track this delegation
    seenSet.add(delegationPath);
    this.seenTasks.set(taskKey, seenSet);

    // Cleanup old entries
    setTimeout(() => {
      this.seenTasks.delete(taskKey);
    }, this.TTL_MS);
  }

  private getTaskKey(envelope: MessageEnvelope): string {
    const message = envelope.payload as AgentMessage;
    return `${envelope.trace_id}:${message.task_id}`;
  }

  incrementDepth(envelope: MessageEnvelope): MessageEnvelope {
    return {
      ...envelope,
      delegation_depth: (envelope.delegation_depth || 0) + 1,
    };
  }
}

Close

Building an agent mesh doesn’t have to create a hairball. Use MCP for tools. Use A2A for agents. Keep the control plane separate from the data plane. Enforce policies at the gateway.

Standards prevent the hairball

Without standards, every agent connects to every tool. Every agent connects to every other agent. The connections multiply. The policies scatter. The failures cascade.

With MCP and A2A, you have:

  • Standard tool connectivity
  • Standard agent communication
  • Clear separation of concerns
  • Manageable complexity

Start with the gateway

The gateway is your control point. It enforces policies. It routes messages. It tracks usage. Build it first. Make all traffic go through it.

Add reliability early

Timeouts, retries, idempotency—add them early. Don’t wait until you have 20 agents and everything is breaking. Build them into the message envelope and the gateway.

Monitor and iterate

Watch for the failure modes. Tool overload. Prompt injection. Infinite loops. Wrong results. Add guards. Add verification. Add limits.

An agent mesh is a distributed system. It will fail. Plan for it. Handle it. Make it observable.

The goal isn’t perfection. It’s manageable complexity. MCP and A2A get you there without building a hairball.

Discussion

Join the conversation and share your thoughts

Discussion

0 / 5000