Skip to content

Commit a7d9d94

Browse files
authored
Fix run hanging when MCP approval is required. (Azure#42548)
* Fix run anging when MCP approval is required. * Fix * Linter fix
1 parent 9a8b337 commit a7d9d94

File tree

4 files changed

+207
-2
lines changed

4 files changed

+207
-2
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
### Bugs Fixed
1414

15+
- Fixed the issue when the `create_and_process` call hangs if MCP tool approval is required.
16+
1517
### Sample updates
1618

1719
- Bing Grounding and Bing Custom Search samples were fixed to correctly present references.

sdk/ai/azure-ai-agents/azure/ai/agents/aio/operations/_patch.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,9 @@ async def create_and_process(
591591
thread_id=thread_id, run_id=run.id, tool_outputs=tool_outputs
592592
)
593593
logger.debug("Tool outputs submitted to run: %s", run2.id)
594+
elif isinstance(run.required_action, _models.SubmitToolApprovalAction):
595+
logger.warning("Automatic MCP tool approval is not supported.")
596+
await self.cancel(thread_id=thread_id, run_id=run.id)
594597

595598
logger.debug("Current run ID: %s with status: %s", run.id, run.status)
596599

@@ -2266,7 +2269,7 @@ async def create_and_poll(
22662269

22672270
@distributed_trace_async
22682271
async def delete(self, vector_store_id: str, file_id: str, **kwargs: Any) -> None:
2269-
"""Deletes a vector store file. This removes the file‐to‐store link (does not delete the file
2272+
"""Deletes a vector store file. This removes the file-to-store link (does not delete the file
22702273
itself).
22712274
22722275
:param vector_store_id: Identifier of the vector store.

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,9 @@ def create_and_process(
590590
if tool_outputs:
591591
run2 = self.submit_tool_outputs(thread_id=thread_id, run_id=run.id, tool_outputs=tool_outputs)
592592
logger.debug("Tool outputs submitted to run: %s", run2.id)
593+
elif isinstance(run.required_action, _models.SubmitToolApprovalAction):
594+
logger.warning("Automatic MCP tool approval is not supported.")
595+
self.cancel(thread_id=thread_id, run_id=run.id)
593596

594597
logger.debug("Current run ID: %s with status: %s", run.id, run.status)
595598

@@ -2265,7 +2268,7 @@ def create_and_poll(
22652268

22662269
@distributed_trace
22672270
def delete(self, vector_store_id: str, file_id: str, **kwargs: Any) -> None:
2268-
"""Deletes a vector store file. This removes the file‐to‐store link (does not delete the file
2271+
"""Deletes a vector store file. This removes the file-to-store link (does not delete the file
22692272
itself).
22702273
22712274
:param vector_store_id: Identifier of the vector store.
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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 the
10+
Model Context Protocol (MCP) tool 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_mcp_async.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 - The URL of your MCP server endpoint.
26+
4) MCP_SERVER_LABEL - A label for your MCP server.
27+
"""
28+
29+
import asyncio
30+
import os
31+
from azure.ai.projects.aio import AIProjectClient
32+
from azure.identity.aio import DefaultAzureCredential
33+
from azure.ai.agents.models import (
34+
ListSortOrder,
35+
McpTool,
36+
RequiredMcpToolCall,
37+
RunStepActivityDetails,
38+
SubmitToolApprovalAction,
39+
ToolApproval,
40+
)
41+
42+
43+
async def main() -> None:
44+
# Get MCP server configuration from environment variables
45+
mcp_server_url = os.environ.get("MCP_SERVER_URL", "https://gitmcp.io/Azure/azure-rest-api-specs")
46+
mcp_server_label = os.environ.get("MCP_SERVER_LABEL", "github")
47+
48+
project_client = AIProjectClient(
49+
endpoint=os.environ["PROJECT_ENDPOINT"],
50+
credential=DefaultAzureCredential(),
51+
)
52+
53+
# Initialize agent MCP tool
54+
mcp_tool = McpTool(
55+
server_label=mcp_server_label,
56+
server_url=mcp_server_url,
57+
allowed_tools=[], # Optional: specify allowed tools
58+
)
59+
# You can also add or remove allowed tools dynamically
60+
search_api_code = "search_azure_rest_api_code"
61+
mcp_tool.allow_tool(search_api_code)
62+
print(f"Allowed tools: {mcp_tool.allowed_tools}")
63+
64+
# Create agent with MCP tool and process agent run
65+
async with project_client:
66+
agents_client = project_client.agents
67+
68+
# Create a new agent.
69+
# NOTE: To reuse existing agent, fetch it with get_agent(agent_id)
70+
agent = await agents_client.create_agent(
71+
model=os.environ["MODEL_DEPLOYMENT_NAME"],
72+
name="my-mcp-agent",
73+
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.",
74+
tools=mcp_tool.definitions,
75+
)
76+
77+
print(f"Created agent, ID: {agent.id}")
78+
print(f"MCP Server: {mcp_tool.server_label} at {mcp_tool.server_url}")
79+
80+
# Create thread for communication
81+
thread = await agents_client.threads.create()
82+
print(f"Created thread, ID: {thread.id}")
83+
84+
# Create message to thread
85+
message = await agents_client.messages.create(
86+
thread_id=thread.id,
87+
role="user",
88+
content="Please summarize the Azure REST API specifications Readme",
89+
)
90+
print(f"Created message, ID: {message.id}")
91+
92+
# Create and process agent run in thread with MCP tools
93+
mcp_tool.update_headers("SuperSecret", "123456")
94+
# mcp_tool.set_approval_mode("never") # Uncomment to disable approval requirement
95+
run = await agents_client.runs.create(thread_id=thread.id, agent_id=agent.id, tool_resources=mcp_tool.resources)
96+
print(f"Created run, ID: {run.id}")
97+
98+
while run.status in ["queued", "in_progress", "requires_action"]:
99+
await asyncio.sleep(1)
100+
run = await agents_client.runs.get(thread_id=thread.id, run_id=run.id)
101+
102+
if run.status == "requires_action" and isinstance(run.required_action, SubmitToolApprovalAction):
103+
tool_calls = run.required_action.submit_tool_approval.tool_calls
104+
if not tool_calls:
105+
print("No tool calls provided - cancelling run")
106+
await agents_client.runs.cancel(thread_id=thread.id, run_id=run.id)
107+
break
108+
109+
tool_approvals = []
110+
for tool_call in tool_calls:
111+
if isinstance(tool_call, RequiredMcpToolCall):
112+
try:
113+
print(f"Approving tool call: {tool_call}")
114+
tool_approvals.append(
115+
ToolApproval(
116+
tool_call_id=tool_call.id,
117+
approve=True,
118+
headers=mcp_tool.headers,
119+
)
120+
)
121+
except Exception as e:
122+
print(f"Error approving tool_call {tool_call.id}: {e}")
123+
124+
print(f"tool_approvals: {tool_approvals}")
125+
if tool_approvals:
126+
await agents_client.runs.submit_tool_outputs(
127+
thread_id=thread.id, run_id=run.id, tool_approvals=tool_approvals
128+
)
129+
130+
print(f"Current run status: {run.status}")
131+
132+
print(f"Run completed with status: {run.status}")
133+
if run.status == "failed":
134+
print(f"Run failed: {run.last_error}")
135+
136+
# Display run steps and tool calls
137+
run_steps = agents_client.run_steps.list(thread_id=thread.id, run_id=run.id)
138+
139+
# Loop through each step
140+
async for step in run_steps:
141+
print(f"Step {step['id']} status: {step['status']}")
142+
143+
# Check if there are tool calls in the step details
144+
step_details = step.get("step_details", {})
145+
tool_calls = step_details.get("tool_calls", [])
146+
147+
if tool_calls:
148+
print(" MCP Tool calls:")
149+
for call in tool_calls:
150+
print(f" Tool Call ID: {call.get('id')}")
151+
print(f" Type: {call.get('type')}")
152+
153+
if isinstance(step_details, RunStepActivityDetails):
154+
for activity in step_details.activities:
155+
for function_name, function_definition in activity.tools.items():
156+
print(
157+
f' The function {function_name} with description "{function_definition.description}" will be called.:'
158+
)
159+
if len(function_definition.parameters) > 0:
160+
print(" Function parameters:")
161+
for argument, func_argument in function_definition.parameters.properties.items():
162+
print(f" {argument}")
163+
print(f" Type: {func_argument.type}")
164+
print(f" Description: {func_argument.description}")
165+
else:
166+
print("This function has no parameters")
167+
168+
print() # add an extra newline between steps
169+
170+
# Fetch and log all messages
171+
messages = agents_client.messages.list(thread_id=thread.id, order=ListSortOrder.ASCENDING)
172+
print("\nConversation:")
173+
print("-" * 50)
174+
async for msg in messages:
175+
if msg.text_messages:
176+
last_text = msg.text_messages[-1]
177+
print(f"{msg.role.upper()}: {last_text.text.value}")
178+
print("-" * 50)
179+
180+
# Example of dynamic tool management
181+
print(f"\nDemonstrating dynamic tool management:")
182+
print(f"Current allowed tools: {mcp_tool.allowed_tools}")
183+
184+
# Remove a tool
185+
try:
186+
mcp_tool.disallow_tool(search_api_code)
187+
print(f"After removing {search_api_code}: {mcp_tool.allowed_tools}")
188+
except ValueError as e:
189+
print(f"Error removing tool: {e}")
190+
191+
# Clean-up and delete the agent once the run is finished.
192+
# NOTE: Comment out this line if you plan to reuse the agent later.
193+
await agents_client.delete_agent(agent.id)
194+
print("Deleted agent")
195+
196+
if __name__ == "__main__":
197+
asyncio.run(main())

0 commit comments

Comments
 (0)