mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix local MCP tools with AzureAIProjectAgentProvider (#3315)
* azureai v2 local mcp fix * addressed copilot comments
This commit is contained in:
committed by
GitHub
Unverified
parent
aa6579f38c
commit
88e0ee1a2c
@@ -14,6 +14,7 @@ from agent_framework import (
|
||||
get_logger,
|
||||
normalize_tools,
|
||||
)
|
||||
from agent_framework._mcp import MCPTool
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
@@ -206,10 +207,32 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
if rai_config:
|
||||
args["rai_config"] = rai_config
|
||||
|
||||
# Normalize tools once and reuse for both Azure AI API and ChatAgent
|
||||
# Normalize tools and separate MCP tools from other tools
|
||||
normalized_tools = normalize_tools(tools)
|
||||
mcp_tools: list[MCPTool] = []
|
||||
non_mcp_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
|
||||
|
||||
if normalized_tools:
|
||||
args["tools"] = to_azure_ai_tools(normalized_tools)
|
||||
for tool in normalized_tools:
|
||||
if isinstance(tool, MCPTool):
|
||||
mcp_tools.append(tool)
|
||||
else:
|
||||
non_mcp_tools.append(tool)
|
||||
|
||||
# Connect MCP tools and discover their functions BEFORE creating the agent
|
||||
# This is required because Azure AI Responses API doesn't accept tools at request time
|
||||
mcp_discovered_functions: list[AIFunction[Any, Any]] = []
|
||||
for mcp_tool in mcp_tools:
|
||||
if not mcp_tool.is_connected:
|
||||
await mcp_tool.connect()
|
||||
mcp_discovered_functions.extend(mcp_tool.functions)
|
||||
|
||||
# Combine non-MCP tools with discovered MCP functions for Azure AI
|
||||
all_tools_for_azure: list[ToolProtocol | MutableMapping[str, Any]] = list(non_mcp_tools)
|
||||
all_tools_for_azure.extend(mcp_discovered_functions)
|
||||
|
||||
if all_tools_for_azure:
|
||||
args["tools"] = to_azure_ai_tools(all_tools_for_azure)
|
||||
|
||||
created_agent = await self._project_client.agents.create_version(
|
||||
agent_name=name,
|
||||
@@ -404,10 +427,12 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
continue
|
||||
merged.append(hosted_tool)
|
||||
|
||||
# Add user-provided function tools (these have the actual implementations)
|
||||
# Add user-provided function tools and MCP tools
|
||||
if provided_tools:
|
||||
for provided_tool in provided_tools:
|
||||
if isinstance(provided_tool, AIFunction):
|
||||
# AIFunction - has implementation for function calling
|
||||
# MCPTool - ChatAgent handles MCP connection and tool discovery at runtime
|
||||
if isinstance(provided_tool, (AIFunction, MCPTool)):
|
||||
merged.append(provided_tool) # type: ignore[reportUnknownArgumentType]
|
||||
|
||||
return merged
|
||||
|
||||
@@ -4,7 +4,8 @@ import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework import AIFunction, ChatAgent
|
||||
from agent_framework._mcp import MCPTool
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
@@ -441,6 +442,170 @@ def test_create_text_format_config_sets_strict_for_pydantic_models() -> None:
|
||||
assert "schema" in result
|
||||
|
||||
|
||||
class MockMCPTool(MCPTool): # pyright: ignore[reportGeneralTypeIssues]
|
||||
"""A mock MCPTool subclass for testing that passes isinstance checks.
|
||||
|
||||
Note: This intentionally does NOT call super().__init__() because MCPTool's
|
||||
constructor requires MCP server connection parameters that aren't needed for
|
||||
unit testing. We only need isinstance(obj, MCPTool) to return True.
|
||||
"""
|
||||
|
||||
def __init__(self, functions: list[AIFunction] | None = None) -> None:
|
||||
self.name = "MockMCPTool"
|
||||
self.description = "A mock MCP tool for testing"
|
||||
self.is_connected = False
|
||||
self._mock_functions = functions or []
|
||||
self._connect_called = False
|
||||
|
||||
@property
|
||||
def functions(self) -> list[AIFunction]:
|
||||
return self._mock_functions
|
||||
|
||||
async def connect(self, *, reset: bool = False) -> None:
|
||||
self._connect_called = True
|
||||
self.is_connected = True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mcp_tool() -> MockMCPTool:
|
||||
"""Fixture that provides a mock MCPTool."""
|
||||
mock_functions = [
|
||||
create_mock_ai_function("mcp_function_1", "First MCP function"),
|
||||
create_mock_ai_function("mcp_function_2", "Second MCP function"),
|
||||
]
|
||||
return MockMCPTool(functions=mock_functions)
|
||||
|
||||
|
||||
def create_mock_ai_function(name: str, description: str = "A mock function") -> AIFunction:
|
||||
"""Create a real AIFunction for testing."""
|
||||
|
||||
def mock_func(arg: str) -> str:
|
||||
return f"Result from {name}: {arg}"
|
||||
|
||||
return AIFunction(func=mock_func, name=name, description=description)
|
||||
|
||||
|
||||
async def test_provider_create_agent_with_mcp_tool(
|
||||
mock_project_client: MagicMock,
|
||||
azure_ai_unit_test_env: dict[str, str],
|
||||
mock_mcp_tool: "MockMCPTool",
|
||||
) -> None:
|
||||
"""Test that create_agent connects MCP tools and passes discovered functions to Azure AI."""
|
||||
|
||||
# Patch normalize_tools to return tools as-is in a list (avoids callable check)
|
||||
def mock_normalize_tools(tools):
|
||||
if tools is None:
|
||||
return []
|
||||
if isinstance(tools, list):
|
||||
return tools
|
||||
return [tools]
|
||||
|
||||
with (
|
||||
patch("agent_framework_azure_ai._project_provider.AzureAISettings") as mock_settings,
|
||||
patch("agent_framework_azure_ai._project_provider.to_azure_ai_tools") as mock_to_azure_tools,
|
||||
patch("agent_framework_azure_ai._project_provider.normalize_tools", side_effect=mock_normalize_tools),
|
||||
):
|
||||
mock_settings.return_value.project_endpoint = azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"]
|
||||
mock_settings.return_value.model_deployment_name = azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]
|
||||
mock_to_azure_tools.return_value = [{"type": "function", "name": "mcp_function_1"}]
|
||||
|
||||
provider = AzureAIProjectAgentProvider(project_client=mock_project_client)
|
||||
|
||||
# Mock agent creation response
|
||||
mock_agent_version = MagicMock(spec=AgentVersionDetails)
|
||||
mock_agent_version.id = "agent-id"
|
||||
mock_agent_version.name = "test-agent"
|
||||
mock_agent_version.version = "1.0"
|
||||
mock_agent_version.description = "Test Agent"
|
||||
mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition)
|
||||
mock_agent_version.definition.model = "gpt-4"
|
||||
mock_agent_version.definition.instructions = "Test instructions"
|
||||
mock_agent_version.definition.tools = []
|
||||
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent_version)
|
||||
|
||||
# Call create_agent with MCP tool
|
||||
await provider.create_agent(
|
||||
name="test-agent",
|
||||
model="gpt-4",
|
||||
instructions="Test instructions",
|
||||
tools=mock_mcp_tool,
|
||||
)
|
||||
|
||||
# Verify MCP tool was connected
|
||||
assert mock_mcp_tool._connect_called is True
|
||||
assert mock_mcp_tool.is_connected is True
|
||||
|
||||
# Verify to_azure_ai_tools was called with the discovered MCP functions
|
||||
mock_to_azure_tools.assert_called_once()
|
||||
tools_passed = mock_to_azure_tools.call_args[0][0]
|
||||
assert len(tools_passed) == 2
|
||||
assert tools_passed[0].name == "mcp_function_1"
|
||||
assert tools_passed[1].name == "mcp_function_2"
|
||||
|
||||
|
||||
async def test_provider_create_agent_with_mcp_and_regular_tools(
|
||||
mock_project_client: MagicMock,
|
||||
azure_ai_unit_test_env: dict[str, str],
|
||||
mock_mcp_tool: "MockMCPTool",
|
||||
) -> None:
|
||||
"""Test that create_agent handles both MCP tools and regular AIFunctions."""
|
||||
# Create a regular AIFunction
|
||||
regular_function = create_mock_ai_function("regular_function", "A regular function")
|
||||
|
||||
# Patch normalize_tools to return tools as-is in a list (avoids callable check)
|
||||
def mock_normalize_tools(tools):
|
||||
if tools is None:
|
||||
return []
|
||||
if isinstance(tools, list):
|
||||
return tools
|
||||
return [tools]
|
||||
|
||||
with (
|
||||
patch("agent_framework_azure_ai._project_provider.AzureAISettings") as mock_settings,
|
||||
patch("agent_framework_azure_ai._project_provider.to_azure_ai_tools") as mock_to_azure_tools,
|
||||
patch("agent_framework_azure_ai._project_provider.normalize_tools", side_effect=mock_normalize_tools),
|
||||
):
|
||||
mock_settings.return_value.project_endpoint = azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"]
|
||||
mock_settings.return_value.model_deployment_name = azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]
|
||||
mock_to_azure_tools.return_value = []
|
||||
|
||||
provider = AzureAIProjectAgentProvider(project_client=mock_project_client)
|
||||
|
||||
# Mock agent creation response
|
||||
mock_agent_version = MagicMock(spec=AgentVersionDetails)
|
||||
mock_agent_version.id = "agent-id"
|
||||
mock_agent_version.name = "test-agent"
|
||||
mock_agent_version.version = "1.0"
|
||||
mock_agent_version.description = None
|
||||
mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition)
|
||||
mock_agent_version.definition.model = "gpt-4"
|
||||
mock_agent_version.definition.instructions = None
|
||||
mock_agent_version.definition.tools = []
|
||||
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent_version)
|
||||
|
||||
# Pass both MCP tool and regular function
|
||||
await provider.create_agent(
|
||||
name="test-agent",
|
||||
model="gpt-4",
|
||||
tools=[mock_mcp_tool, regular_function],
|
||||
)
|
||||
|
||||
# Verify to_azure_ai_tools was called with:
|
||||
# - The regular AIFunction (1)
|
||||
# - The 2 discovered MCP functions
|
||||
mock_to_azure_tools.assert_called_once()
|
||||
tools_passed = mock_to_azure_tools.call_args[0][0]
|
||||
assert len(tools_passed) == 3 # 1 regular + 2 MCP functions
|
||||
|
||||
# Verify the regular function is in the list
|
||||
tool_names = [t.name for t in tools_passed]
|
||||
assert "regular_function" in tool_names
|
||||
assert "mcp_function_1" in tool_names
|
||||
assert "mcp_function_2" in tool_names
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_provider_create_and_get_agent_integration() -> None:
|
||||
|
||||
@@ -22,6 +22,11 @@ async def main() -> None:
|
||||
"""Example showing use of Local MCP Tool with AzureAIProjectAgentProvider."""
|
||||
print("=== Azure AI Agent with Local MCP Tools Example ===\n")
|
||||
|
||||
mcp_tool = MCPStreamableHTTPTool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
)
|
||||
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIProjectAgentProvider(credential=credential) as provider,
|
||||
@@ -29,23 +34,22 @@ async def main() -> None:
|
||||
agent = await provider.create_agent(
|
||||
name="DocsAgent",
|
||||
instructions="You are a helpful assistant that can help with Microsoft documentation questions.",
|
||||
tools=MCPStreamableHTTPTool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
),
|
||||
tools=mcp_tool,
|
||||
)
|
||||
|
||||
# First query
|
||||
first_query = "How to create an Azure storage account using az cli?"
|
||||
print(f"User: {first_query}")
|
||||
first_result = await agent.run(first_query)
|
||||
print(f"Agent: {first_result}")
|
||||
print("\n=======================================\n")
|
||||
# Second query
|
||||
second_query = "What is Microsoft Agent Framework?"
|
||||
print(f"User: {second_query}")
|
||||
second_result = await agent.run(second_query)
|
||||
print(f"Agent: {second_result}")
|
||||
# Use agent as context manager to ensure proper cleanup
|
||||
async with agent:
|
||||
# First query
|
||||
first_query = "How to create an Azure storage account using az cli?"
|
||||
print(f"User: {first_query}")
|
||||
first_result = await agent.run(first_query)
|
||||
print(f"Agent: {first_result}")
|
||||
print("\n=======================================\n")
|
||||
# Second query
|
||||
second_query = "What is Microsoft Agent Framework?"
|
||||
print(f"User: {second_query}")
|
||||
second_result = await agent.run(second_query)
|
||||
print(f"Agent: {second_result}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user