Skip to content

Commit 4eeba58

Browse files
authored
[Agents] add static merge_resources method to McpTool class (#42768)
1 parent bb2365c commit 4eeba58

File tree

3 files changed

+242
-1
lines changed

3 files changed

+242
-1
lines changed

sdk/ai/azure-ai-agents/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88

99
### Features Added
1010

11+
- Added static merge_resources method to `McpTool` with accompanying sample.
12+
1113
### Bugs Fixed
1214

1315
* Fix the issue with logging Agent message, when the message has "in progress" status (related to [issue](https://github.com/Azure/azure-sdk-for-python/issues/42645)).
1416
* Fix the issue with `RunStepOpenAPIToolCall` logging [issue](https://github.com/Azure/azure-sdk-for-python/issues/42645).
15-
* Fix the issue with `RunStepMcpToolCall` logging [issue](https://github.com/Azure/azure-sdk-for-python/issues/42689).
1617

1718
### Sample updates
1819

20+
- Added sample demonstrating multiple McpTool instance usage.
21+
1922
## 1.2.0b3 (2025-08-22)
2023

2124
### Features Added

sdk/ai/azure-ai-agents/azure/ai/agents/models/_patch.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,36 @@ def execute(self, tool_call: Any) -> None:
971971
:type tool_call: Any
972972
"""
973973

974+
@staticmethod
975+
def merge_resources(mcp_tools: List["McpTool"]) -> ToolResources:
976+
"""
977+
Merge the tool resources from multiple MCP tool instances into a single ToolResources object.
978+
979+
This is useful when creating a run that should have access to multiple MCP servers at once.
980+
981+
:param mcp_tools: A list of McpTool instances whose resources will be merged.
982+
:type mcp_tools: List[McpTool]
983+
:return: A ToolResources object containing all MCP tool resources from the provided tools.
984+
:rtype: ToolResources
985+
:raises ValueError: If the provided list is empty.
986+
:raises TypeError: If any item in the list is not an instance of McpTool.
987+
"""
988+
if not mcp_tools:
989+
raise ValueError("mcp_tools must be a non-empty list of McpTool instances.")
990+
991+
flat_resources: List[MCPToolResource] = []
992+
for tool in mcp_tools:
993+
if not isinstance(tool, McpTool):
994+
raise TypeError("All items in mcp_tools must be instances of McpTool.")
995+
# Combine all MCP resources; duplicates are harmless and can be filtered by the service if needed
996+
res = tool.resources.get("mcp") # May be a list or a single MCPToolResource depending on model behavior
997+
if isinstance(res, list):
998+
flat_resources.extend(cast(List[MCPToolResource], res))
999+
elif res is not None:
1000+
flat_resources.append(cast(MCPToolResource, res))
1001+
1002+
return ToolResources(mcp=flat_resources)
1003+
9741004

9751005
class AzureFunctionTool(Tool[AzureFunctionToolDefinition]):
9761006
"""
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# pylint: disable=line-too-long,useless-suppression
2+
# ------------------------------------
3+
# Copyright (c) Microsoft Corporation.
4+
# Licensed under the MIT License.
5+
# ------------------------------------
6+
7+
"""
8+
DESCRIPTION:
9+
This sample demonstrates how to use agent operations with multiple
10+
Model Context Protocol (MCP) servers from the Azure Agents service using a synchronous client.
11+
To learn more about Model Context Protocol, visit https://modelcontextprotocol.io/
12+
13+
USAGE:
14+
python sample_agents_multiple_mcp.py
15+
16+
Before running the sample:
17+
18+
pip install azure-ai-projects azure-ai-agents>=1.2.0b3 azure-identity --pre
19+
20+
Set these environment variables with your own values:
21+
1) PROJECT_ENDPOINT - The Azure AI Project endpoint, as found in the Overview
22+
page of your Azure AI Foundry portal.
23+
2) MODEL_DEPLOYMENT_NAME - The deployment name of the AI model, as found under the "Name" column in
24+
the "Models + endpoints" tab in your Azure AI Foundry project.
25+
3) MCP_SERVER_URL_1 - The URL of your first MCP server endpoint.
26+
4) MCP_SERVER_LABEL_1 - A label for your first MCP server.
27+
5) MCP_SERVER_URL_2 - The URL of your second MCP server endpoint.
28+
6) MCP_SERVER_LABEL_2 - A label for your second MCP server.
29+
"""
30+
31+
import os, time
32+
from azure.ai.projects import AIProjectClient
33+
from azure.identity import DefaultAzureCredential
34+
from azure.ai.agents.models import (
35+
ListSortOrder,
36+
McpTool,
37+
RequiredMcpToolCall,
38+
RunStepActivityDetails,
39+
SubmitToolApprovalAction,
40+
ToolApproval,
41+
)
42+
43+
# Get MCP server configuration from environment variables
44+
mcp_server_url_1 = os.environ.get("MCP_SERVER_URL_1", "https://gitmcp.io/Azure/azure-rest-api-specs")
45+
mcp_server_label_1 = os.environ.get("MCP_SERVER_LABEL_1", "github")
46+
mcp_server_url_2 = os.environ.get("MCP_SERVER_URL_2", "https://learn.microsoft.com/api/mcp")
47+
mcp_server_label_2 = os.environ.get("MCP_SERVER_LABEL_2", "microsoft_learn")
48+
49+
50+
project_client = AIProjectClient(
51+
endpoint=os.environ["PROJECT_ENDPOINT"],
52+
credential=DefaultAzureCredential(),
53+
)
54+
55+
# Initialize agent MCP tool
56+
mcp_tool1 = McpTool(
57+
server_label=mcp_server_label_1,
58+
server_url=mcp_server_url_1,
59+
allowed_tools=[], # Optional: specify allowed tools
60+
)
61+
62+
# You can also add or remove allowed tools dynamically
63+
search_api_code = "search_azure_rest_api_code"
64+
mcp_tool1.allow_tool(search_api_code)
65+
print(f"Allowed tools: {mcp_tool1.allowed_tools}")
66+
67+
mcp_tool2 = McpTool(
68+
server_label=mcp_server_label_2,
69+
server_url=mcp_server_url_2,
70+
allowed_tools=["microsoft_docs_search"], # Optional: specify allowed tools
71+
)
72+
73+
mcp_tools = [mcp_tool1, mcp_tool2]
74+
75+
# Create agent with MCP tool and process agent run
76+
with project_client:
77+
agents_client = project_client.agents
78+
79+
# Create a new agent.
80+
# NOTE: To reuse existing agent, fetch it with get_agent(agent_id)
81+
agent = agents_client.create_agent(
82+
model=os.environ["MODEL_DEPLOYMENT_NAME"],
83+
name="my-mcp-agent",
84+
instructions="You are a helpful agent that can use MCP tools to assist users. Use the available MCP tools to answer questions and perform tasks.",
85+
tools=[tool.definitions[0] for tool in mcp_tools],
86+
)
87+
88+
print(f"Created agent, ID: {agent.id}")
89+
print(f"MCP Server 1: {mcp_tool1.server_label} at {mcp_tool1.server_url}")
90+
print(f"MCP Server 2: {mcp_tool2.server_label} at {mcp_tool2.server_url}")
91+
92+
# Create thread for communication
93+
thread = agents_client.threads.create()
94+
print(f"Created thread, ID: {thread.id}")
95+
96+
# Create message to thread
97+
message = agents_client.messages.create(
98+
thread_id=thread.id,
99+
role="user",
100+
content="Please summarize the Azure REST API specifications Readme and Give me the Azure CLI commands to create an Azure Container App with a managed identity",
101+
)
102+
print(f"Created message, ID: {message.id}")
103+
104+
# Create and process agent run in thread with MCP tools
105+
mcp_tool1.update_headers("SuperSecret", "123456")
106+
mcp_tool2.set_approval_mode("never") # Disable approval for MS Learn MCP tool
107+
tool_resources = McpTool.merge_resources(mcp_tools)
108+
print(tool_resources)
109+
run = agents_client.runs.create(thread_id=thread.id, agent_id=agent.id, tool_resources=tool_resources)
110+
print(f"Created run, ID: {run.id}")
111+
112+
while run.status in ["queued", "in_progress", "requires_action"]:
113+
time.sleep(1)
114+
run = agents_client.runs.get(thread_id=thread.id, run_id=run.id)
115+
116+
if run.status == "requires_action" and isinstance(run.required_action, SubmitToolApprovalAction):
117+
tool_calls = run.required_action.submit_tool_approval.tool_calls
118+
if not tool_calls:
119+
print("No tool calls provided - cancelling run")
120+
agents_client.runs.cancel(thread_id=thread.id, run_id=run.id)
121+
break
122+
123+
tool_approvals = []
124+
for tool_call in tool_calls:
125+
if isinstance(tool_call, RequiredMcpToolCall):
126+
try:
127+
print(f"Approving tool call: {tool_call}")
128+
tool_approvals.append(
129+
ToolApproval(
130+
tool_call_id=tool_call.id,
131+
approve=True,
132+
headers=mcp_tool1.headers,
133+
)
134+
)
135+
except Exception as e:
136+
print(f"Error approving tool_call {tool_call.id}: {e}")
137+
138+
print(f"tool_approvals: {tool_approvals}")
139+
if tool_approvals:
140+
agents_client.runs.submit_tool_outputs(
141+
thread_id=thread.id, run_id=run.id, tool_approvals=tool_approvals
142+
)
143+
144+
print(f"Current run status: {run.status}")
145+
146+
print(f"Run completed with status: {run.status}")
147+
if run.status == "failed":
148+
print(f"Run failed: {run.last_error}")
149+
150+
# Display run steps and tool calls
151+
run_steps = agents_client.run_steps.list(thread_id=thread.id, run_id=run.id)
152+
153+
# Loop through each step
154+
for step in run_steps:
155+
print(f"Step {step['id']} status: {step['status']}")
156+
157+
# Check if there are tool calls in the step details
158+
step_details = step.get("step_details", {})
159+
tool_calls = step_details.get("tool_calls", [])
160+
161+
if tool_calls:
162+
print(" MCP Tool calls:")
163+
for call in tool_calls:
164+
print(f" Tool Call ID: {call.get('id')}")
165+
print(f" Type: {call.get('type')}")
166+
167+
if isinstance(step_details, RunStepActivityDetails):
168+
for activity in step_details.activities:
169+
for function_name, function_definition in activity.tools.items():
170+
print(
171+
f' The function {function_name} with description "{function_definition.description}" will be called.:'
172+
)
173+
if len(function_definition.parameters) > 0:
174+
print(" Function parameters:")
175+
for argument, func_argument in function_definition.parameters.properties.items():
176+
print(f" {argument}")
177+
print(f" Type: {func_argument.type}")
178+
print(f" Description: {func_argument.description}")
179+
else:
180+
print("This function has no parameters")
181+
182+
print() # add an extra newline between steps
183+
184+
# Fetch and log all messages
185+
messages = agents_client.messages.list(thread_id=thread.id, order=ListSortOrder.ASCENDING)
186+
print("\nConversation:")
187+
print("-" * 50)
188+
for msg in messages:
189+
if msg.text_messages:
190+
last_text = msg.text_messages[-1]
191+
print(f"{msg.role.upper()}: {last_text.text.value}")
192+
print("-" * 50)
193+
194+
# Example of dynamic tool management
195+
print(f"\nDemonstrating dynamic tool management:")
196+
print(f"Current allowed tools: {mcp_tool1.allowed_tools}")
197+
198+
# Remove a tool
199+
try:
200+
mcp_tool1.disallow_tool(search_api_code)
201+
print(f"After removing {search_api_code}: {mcp_tool1.allowed_tools}")
202+
except ValueError as e:
203+
print(f"Error removing tool: {e}")
204+
205+
# Clean-up and delete the agent once the run is finished.
206+
# NOTE: Comment out this line if you plan to reuse the agent later.
207+
agents_client.delete_agent(agent.id)
208+
print("Deleted agent")

0 commit comments

Comments
 (0)