Skip to content

Commit 975d7f9

Browse files
author
Anivar A Aravind
committed
feat: Add MCP-to-A2A bridge for protocol interoperability
This PR introduces a bridge that allows MCP (Model Context Protocol) servers to be exposed as A2A (Agent-to-Agent) compatible services, enabling seamless protocol interoperability between the two major agent communication standards. ## Implementation ### Core Components - MCPBridgeExecutor: Translates A2A requests to MCP tool calls - MCPToA2ABridgeConfig: Configuration for bridge settings - serve_mcp_as_a2a_async: Main function to start the bridge server ### Features - Full protocol translation between MCP and A2A - Support for AGNTCY Identity (tracked internally, not exposed via A2A due to protocol limitations) - Comprehensive error handling and logging - Clean integration with existing any-agent patterns - Zero custom rules or patterns - follows any-agent conventions exactly ### Documentation - Added detailed section in docs/serving.md - Created cookbook example (mcp_a2a_bridge.ipynb) with practical usage patterns - Added bridge feature to README.md - Complete example script in examples/mcp_a2a_bridge_demo.py ### Testing - Integration tests covering basic bridge, identity support, and tool invocation - Test for multiple concurrent bridges - All tests properly handle the simplified single-tool bridge design ## Design Decisions 1. Single-tool bridges only - Keeps implementation simple and follows any-agent patterns 2. Identity support - Accepts AGNTCY Identity DIDs from mcpd but acknowledges A2A limitations 3. No custom patterns - Strictly follows existing any-agent conventions 4. Clean separation - Bridge is separate from native A2A serving implementation ## Usage Example ```python from any_agent.config import MCPStdio from any_agent.serving import MCPToA2ABridgeConfig, serve_mcp_as_a2a_async # Configure MCP server mcp_config = MCPStdio( command="uvx", args=["mcp-server-time"], tools=["get_current_time"], ) # Configure and start bridge bridge_config = MCPToA2ABridgeConfig( mcp_config=mcp_config, port=8080, endpoint="/time-bridge", server_name="time-server", ) bridge_handle = await serve_mcp_as_a2a_async(mcp_config, bridge_config) ``` This bridge enables Mozilla's any-agent to connect local MCP tools with Google's A2A ecosystem, providing a unified interface for agent communication across protocols.
1 parent b7cd567 commit 975d7f9

File tree

9 files changed

+1029
-2
lines changed

9 files changed

+1029
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Get started quickly with these practical examples:
9393
- **[Creating an agent with MCP](https://mozilla-ai.github.io/any-agent/cookbook/mcp_agent/)** - Integrate Model Context Protocol tools.
9494
- **[Serve an Agent with A2A](https://mozilla-ai.github.io/any-agent/cookbook/serve_a2a/)** - Deploy agents with Agent-to-Agent communication.
9595
- **[Building Multi-Agent Systems with A2A](https://mozilla-ai.github.io/any-agent/cookbook/a2a_as_tool/)** - Using an agent as a tool for another agent to interact with.
96+
- **[Bridge MCP servers to A2A](https://mozilla-ai.github.io/any-agent/cookbook/mcp_a2a_bridge/)** - Expose MCP tools as A2A-compatible services for protocol interoperability.
9697

9798
## Contributions
9899

docs/cookbook/mcp_a2a_bridge.ipynb

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Bridge MCP Servers to A2A Protocol\n",
8+
"\n",
9+
"This cookbook demonstrates how to expose MCP (Model Context Protocol) servers as A2A-compatible services, enabling seamless interoperability between different agent ecosystems.\n",
10+
"\n",
11+
"## Why Bridge MCP to A2A?\n",
12+
"\n",
13+
"- **Protocol Interoperability**: Make MCP tools available to Google's A2A ecosystem\n",
14+
"- **Enterprise Integration**: Connect local tools with enterprise agent systems\n",
15+
"- **Identity Support**: Leverage AGNTCY Identity for secure tool access\n",
16+
"- **Zero Changes**: MCP servers work as-is without modifications\n",
17+
"\n",
18+
"This tutorial assumes familiarity with both MCP and A2A. If you're new to these protocols:\n",
19+
"- MCP: See [Creating an agent with MCP](./mcp_agent.ipynb)\n",
20+
"- A2A: See [Serve an Agent with A2A](./serve_a2a.ipynb)"
21+
]
22+
},
23+
{
24+
"cell_type": "markdown",
25+
"metadata": {},
26+
"source": [
27+
"## Install Dependencies\n",
28+
"\n",
29+
"You'll need both MCP and A2A support:"
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"%pip install 'any-agent[mcp,a2a]'\n",
39+
"\n",
40+
"import nest_asyncio\n",
41+
"nest_asyncio.apply()"
42+
]
43+
},
44+
{
45+
"cell_type": "code",
46+
"execution_count": null,
47+
"metadata": {},
48+
"outputs": [],
49+
"source": [
50+
"import os\n",
51+
"from getpass import getpass\n",
52+
"\n",
53+
"# This example uses Mistral models\n",
54+
"if \"MISTRAL_API_KEY\" not in os.environ:\n",
55+
" print(\"MISTRAL_API_KEY not found in environment!\")\n",
56+
" api_key = getpass(\"Please enter your MISTRAL_API_KEY: \")\n",
57+
" os.environ[\"MISTRAL_API_KEY\"] = api_key\n",
58+
" print(\"MISTRAL_API_KEY set for this session!\")\n",
59+
"else:\n",
60+
" print(\"MISTRAL_API_KEY found in environment.\")"
61+
]
62+
},
63+
{
64+
"cell_type": "markdown",
65+
"metadata": {},
66+
"source": [
67+
"## Basic Bridge: Single MCP Server\n",
68+
"\n",
69+
"Let's start by bridging a single MCP server (time server) to A2A:"
70+
]
71+
},
72+
{
73+
"cell_type": "code",
74+
"metadata": {},
75+
"outputs": [],
76+
"source": "from any_agent.config import MCPStdio\nfrom any_agent.serving import (\n MCPToA2ABridgeConfig,\n serve_mcp_as_a2a_async,\n)\n\n# Configure MCP server\nmcp_config = MCPStdio(\n command=\"uvx\",\n args=[\"mcp-server-time\", \"--local-timezone=America/New_York\"],\n tools=[\"get_current_time\"],\n)\n\n# Configure bridge\nbridge_config = MCPToA2ABridgeConfig(\n mcp_config=mcp_config,\n port=0, # Use dynamic port\n endpoint=\"/time-bridge\",\n server_name=\"time-server\",\n)\n\n# Start the bridge\nbridge_handle = await serve_mcp_as_a2a_async(mcp_config, bridge_config)\nbridge_port = bridge_handle.port\n\nprint(f\"✓ MCP time server bridged to A2A at: http://localhost:{bridge_port}/time-bridge\")"
77+
},
78+
{
79+
"cell_type": "markdown",
80+
"metadata": {},
81+
"source": [
82+
"## Use the Bridged MCP Tool via A2A\n",
83+
"\n",
84+
"Now we can access the MCP tool through A2A protocol:"
85+
]
86+
},
87+
{
88+
"cell_type": "code",
89+
"execution_count": null,
90+
"metadata": {},
91+
"outputs": [],
92+
"source": [
93+
"from any_agent import AgentConfig, AnyAgent\n",
94+
"from any_agent.tools import a2a_tool_async\n",
95+
"\n",
96+
"# Create an A2A tool from the bridged MCP server\n",
97+
"time_tool = await a2a_tool_async(\n",
98+
" f\"http://localhost:{bridge_port}/time-bridge\",\n",
99+
" toolname=\"get_time_via_a2a\",\n",
100+
" http_kwargs={\"timeout\": 30},\n",
101+
")\n",
102+
"\n",
103+
"# Create an agent that uses the bridged tool\n",
104+
"agent = await AnyAgent.create_async(\n",
105+
" \"tinyagent\",\n",
106+
" AgentConfig(\n",
107+
" model_id=\"mistral/mistral-small-latest\",\n",
108+
" instructions=\"Use the available tools to answer questions about time.\",\n",
109+
" tools=[time_tool],\n",
110+
" ),\n",
111+
")\n",
112+
"\n",
113+
"# Use the MCP tool through A2A\n",
114+
"result = await agent.run_async(\"What time is it in New York?\")\n",
115+
"print(f\"Agent response: {result.final_output}\")"
116+
]
117+
},
118+
{
119+
"cell_type": "markdown",
120+
"metadata": {},
121+
"source": "## Advanced: Bridge with AGNTCY Identity\n\nIf you're using mcpd with AGNTCY Identity support, you can include identity information in the bridge configuration. While the identity won't be exposed through the A2A protocol (due to AgentCard limitations), it will be logged and can be used for internal tracking:",
122+
"outputs": []
123+
},
124+
{
125+
"cell_type": "code",
126+
"metadata": {},
127+
"outputs": [],
128+
"source": "import json\nfrom pathlib import Path\n\n# Check if identity exists (from mcpd)\nidentity_path = Path(\"~/.config/mcpd/identity/time-server.json\").expanduser()\nidentity_id = None\n\nif identity_path.exists():\n with open(identity_path) as f:\n identity_data = json.load(f)\n identity_id = identity_data.get(\"id\")\n print(f\"Found identity: {identity_id}\")\nelse:\n print(\"No identity found - proceeding without identity\")\n\n# Create bridge with identity\nbridge_config_with_id = MCPToA2ABridgeConfig(\n mcp_config=mcp_config,\n port=0,\n endpoint=\"/secure-time\",\n server_name=\"time-server\",\n identity_id=identity_id, # Will be logged but not exposed via A2A\n)\n\n# The identity will be logged when the bridge starts\nsecure_bridge = await serve_mcp_as_a2a_async(mcp_config, bridge_config_with_id)\nprint(f\"✓ Secure bridge started (check logs for identity verification)\")"
129+
},
130+
{
131+
"cell_type": "markdown",
132+
"metadata": {},
133+
"source": [
134+
"## Multi-Bridge: Orchestrating Multiple MCP Tools\n",
135+
"\n",
136+
"Let's bridge multiple MCP servers and orchestrate them:"
137+
]
138+
},
139+
{
140+
"cell_type": "code",
141+
"execution_count": null,
142+
"metadata": {},
143+
"outputs": [],
144+
"source": [
145+
"# Bridge multiple MCP servers\n",
146+
"bridges = []\n",
147+
"tools = []\n",
148+
"\n",
149+
"# Time server\n",
150+
"time_bridge = await serve_mcp_as_a2a_async(\n",
151+
" MCPStdio(\n",
152+
" command=\"uvx\",\n",
153+
" args=[\"mcp-server-time\"],\n",
154+
" tools=[\"get_current_time\"],\n",
155+
" ),\n",
156+
" MCPToA2ABridgeConfig(\n",
157+
" mcp_config=None, # Will be set by serve function\n",
158+
" port=0,\n",
159+
" endpoint=\"/time\",\n",
160+
" server_name=\"time\",\n",
161+
" ),\n",
162+
")\n",
163+
"bridges.append(time_bridge)\n",
164+
"tools.append(\n",
165+
" await a2a_tool_async(\n",
166+
" f\"http://localhost:{time_bridge.port}/time\",\n",
167+
" toolname=\"time_tool\",\n",
168+
" )\n",
169+
")\n",
170+
"\n",
171+
"# Add more MCP servers here (filesystem, github, etc.)\n",
172+
"# Example: filesystem_bridge = await serve_mcp_as_a2a_async(...)\n",
173+
"\n",
174+
"print(f\"✓ Bridged {len(bridges)} MCP servers to A2A\")\n",
175+
"\n",
176+
"# Create orchestrator agent with all bridged tools\n",
177+
"orchestrator = await AnyAgent.create_async(\n",
178+
" \"tinyagent\",\n",
179+
" AgentConfig(\n",
180+
" model_id=\"mistral/mistral-small-latest\",\n",
181+
" name=\"orchestrator\",\n",
182+
" instructions=\"Coordinate multiple tools to answer complex questions.\",\n",
183+
" tools=tools,\n",
184+
" ),\n",
185+
")\n",
186+
"\n",
187+
"# Complex query using multiple tools\n",
188+
"result = await orchestrator.run_async(\n",
189+
" \"What's the current time? Format it nicely for a report.\"\n",
190+
")\n",
191+
"print(f\"\\nOrchestrator response:\\n{result.final_output}\")"
192+
]
193+
},
194+
{
195+
"cell_type": "markdown",
196+
"metadata": {},
197+
"source": [
198+
"## Direct Tool Invocation\n",
199+
"\n",
200+
"The bridge also supports direct tool invocation using a simple protocol:"
201+
]
202+
},
203+
{
204+
"cell_type": "code",
205+
"metadata": {},
206+
"outputs": [],
207+
"source": "import httpx\nfrom a2a.client import A2AClient, A2ACardResolver\nfrom a2a.types import (\n Message,\n MessageSendParams,\n Part,\n Role,\n SendMessageRequest,\n TextPart,\n)\nfrom uuid import uuid4\n\n# Get the agent card\nbridge_url = f\"http://localhost:{bridge_port}/time-bridge\"\nasync with httpx.AsyncClient() as client:\n resolver = A2ACardResolver(httpx_client=client, base_url=bridge_url)\n agent_card = await resolver.get_agent_card()\n print(f\"Agent: {agent_card.name}\")\n print(f\"Skills: {[s.name for s in agent_card.skills]}\")\n\n# Direct tool call\nasync with httpx.AsyncClient() as client:\n a2a_client = A2AClient(httpx_client=client, agent_card=agent_card)\n \n # For single tool bridges, just send the input\n request = SendMessageRequest(\n id=str(uuid4()),\n params=MessageSendParams(\n message=Message(\n role=Role.user,\n parts=[Part(root=TextPart(text=\"current time\"))],\n message_id=str(uuid4()),\n )\n ),\n )\n \n response = await a2a_client.send_message(request)\n print(f\"\\nDirect response: {response}\")"
208+
},
209+
{
210+
"cell_type": "markdown",
211+
"metadata": {},
212+
"source": [
213+
"## Cleanup\n",
214+
"\n",
215+
"Don't forget to clean up the bridges:"
216+
]
217+
},
218+
{
219+
"cell_type": "code",
220+
"execution_count": null,
221+
"metadata": {},
222+
"outputs": [],
223+
"source": [
224+
"# Shutdown all bridges\n",
225+
"for bridge in bridges:\n",
226+
" await bridge.shutdown()\n",
227+
"\n",
228+
"await bridge_handle.shutdown()\n",
229+
"await secure_bridge.shutdown()\n",
230+
"\n",
231+
"print(\"✓ All bridges shut down\")"
232+
]
233+
},
234+
{
235+
"cell_type": "markdown",
236+
"metadata": {},
237+
"source": [
238+
"## Next Steps\n",
239+
"\n",
240+
"1. **Production Deployment**: Deploy bridges as microservices\n",
241+
"2. **Service Discovery**: Register bridges with AGNTCY Directory\n",
242+
"3. **Authentication**: Add proper auth between bridges and agents\n",
243+
"4. **Monitoring**: Add observability for bridge operations\n",
244+
"\n",
245+
"For more information:\n",
246+
"- [MCP Documentation](https://modelcontextprotocol.io/)\n",
247+
"- [A2A Protocol](https://github.com/google/a2a)\n",
248+
"- [AGNTCY Identity](https://github.com/agntcy/identity)"
249+
]
250+
}
251+
],
252+
"metadata": {
253+
"kernelspec": {
254+
"display_name": "Python 3",
255+
"language": "python",
256+
"name": "python3"
257+
},
258+
"language_info": {
259+
"codemirror_mode": {
260+
"name": "ipython",
261+
"version": 3
262+
},
263+
"file_extension": ".py",
264+
"mimetype": "text/x-python",
265+
"name": "python",
266+
"nbconvert_exporter": "python",
267+
"pygments_lexer": "ipython3",
268+
"version": "3.11.0"
269+
}
270+
},
271+
"nbformat": 4,
272+
"nbformat_minor": 4
273+
}

docs/serving.md

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
- [Model Context Protocol (MCP)](https://modelcontextprotocol.io/specification/2025-03-26), via the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).
99

10+
- **MCP to A2A Bridge**: Expose MCP servers as A2A-compatible services, enabling protocol interoperability. Requires both extras: `pip install 'any-agent[mcp,a2a]'`.
11+
1012
## Configuring and Serving Agents
1113

1214
You can configure and serve an agent using the [`A2AServingConfig`][any_agent.serving.A2AServingConfig] or [`MCPServingConfig`][any_agent.serving.MCPServingConfig] and the [`AnyAgent.serve_async`][any_agent.AnyAgent.serve_async] method.
@@ -152,10 +154,86 @@ if __name__ == "__main__":
152154

153155
```
154156

157+
## Bridging MCP to A2A
158+
159+
any-agent provides a bridge that allows you to expose MCP servers as A2A-compatible services. This enables:
160+
161+
- **Protocol Interoperability**: Make MCP tools available to Google's A2A ecosystem
162+
- **Enterprise Integration**: Connect local tools with enterprise agent systems
163+
- **Identity Support**: Leverage AGNTCY Identity for secure tool access
164+
165+
### Basic Bridge Example
166+
167+
```python
168+
import asyncio
169+
from any_agent.config import MCPStdio
170+
from any_agent.serving import (
171+
MCPToA2ABridgeConfig,
172+
serve_mcp_as_a2a_async,
173+
)
174+
175+
async def bridge_mcp_server():
176+
# Configure MCP server
177+
mcp_config = MCPStdio(
178+
command="uvx",
179+
args=["mcp-server-time"],
180+
tools=["get_current_time"],
181+
)
182+
183+
# Configure bridge
184+
bridge_config = MCPToA2ABridgeConfig(
185+
mcp_config=mcp_config,
186+
port=8080,
187+
endpoint="/time-bridge",
188+
server_name="time-server",
189+
)
190+
191+
# Start the bridge
192+
bridge_handle = await serve_mcp_as_a2a_async(mcp_config, bridge_config)
193+
print(f"MCP server bridged to A2A at: http://localhost:8080/time-bridge")
194+
195+
# Keep running
196+
await bridge_handle.task
197+
198+
if __name__ == "__main__":
199+
asyncio.run(bridge_mcp_server())
200+
```
201+
202+
### Using the Bridged MCP Tool
203+
204+
Once bridged, the MCP tool can be accessed via A2A protocol:
205+
206+
```python
207+
from any_agent import AgentConfig, AnyAgent
208+
from any_agent.tools import a2a_tool_async
209+
210+
async def use_bridged_tool():
211+
# Create A2A tool from bridged MCP server
212+
time_tool = await a2a_tool_async(
213+
"http://localhost:8080/time-bridge",
214+
toolname="get_time_via_bridge",
215+
)
216+
217+
# Use in an agent
218+
agent = await AnyAgent.create_async(
219+
"tinyagent",
220+
AgentConfig(
221+
model_id="mistral/mistral-small-latest",
222+
instructions="Use tools to answer questions.",
223+
tools=[time_tool],
224+
),
225+
)
226+
227+
result = await agent.run_async("What time is it?")
228+
print(result.final_output)
229+
```
230+
155231
## More Examples
156232

157-
Check out our cookbook example for building and serving an agent via A2A:
233+
Check out our cookbook examples:
158234

159235
👉 [Serve an Agent with A2A (Jupyter Notebook)](./cookbook/serve_a2a.ipynb)
160236

161-
👉 [Use an A2a Agent as a tool (Jupyter Notebook)](./cookbook/serve_a2a.ipynb)
237+
👉 [Use an A2A Agent as a tool (Jupyter Notebook)](./cookbook/a2a_as_tool.ipynb)
238+
239+
👉 [Bridge MCP servers to A2A (Jupyter Notebook)](./cookbook/mcp_a2a_bridge.ipynb)

0 commit comments

Comments
 (0)