diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 00b9fc9348..0c5462687f 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft. All rights reserved. import sys -from collections.abc import MutableSequence +from collections.abc import MutableMapping, MutableSequence from typing import Any, ClassVar, TypeVar from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, ChatMessage, ChatOptions, + HostedMCPTool, TextContent, get_logger, use_chat_middleware, @@ -18,6 +19,7 @@ from agent_framework.openai._responses_client import OpenAIBaseResponsesClient from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( + MCPTool, PromptAgentDefinition, PromptAgentDefinitionText, ResponseTextFormatConfigurationJsonSchema, @@ -325,3 +327,27 @@ def _update_agent_name(self, agent_name: str | None) -> None: # to update the agent name in the client. if agent_name and not self.agent_name: self.agent_name = agent_name + + def get_mcp_tool(self, tool: HostedMCPTool) -> MutableMapping[str, Any]: + """Get MCP tool from HostedMCPTool.""" + mcp: MCPTool = { + "type": "mcp", + "server_label": tool.name.replace(" ", "_"), + "server_url": str(tool.url), + } + + if tool.allowed_tools: + mcp["allowed_tools"] = list(tool.allowed_tools) + + # TODO (dmytrostruk): Check "always" approval mode + if tool.approval_mode: + match tool.approval_mode: + case str(): + mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never" + case _: + if always_require_approvals := tool.approval_mode.get("always_require_approval"): + mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}} + if never_require_approvals := tool.approval_mode.get("never_require_approval"): + mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} + + return mcp diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index 647963dd97..08f370cc24 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -187,31 +187,7 @@ def _tools_to_response_tools( if isinstance(tool, ToolProtocol): match tool: case HostedMCPTool(): - mcp: Mcp = { - "type": "mcp", - "server_label": tool.name.replace(" ", "_"), - "server_url": str(tool.url), - "server_description": tool.description, - "headers": tool.headers, - } - if tool.allowed_tools: - mcp["allowed_tools"] = list(tool.allowed_tools) - if tool.approval_mode: - match tool.approval_mode: - case str(): - mcp["require_approval"] = ( - "always" if tool.approval_mode == "always_require" else "never" - ) - case _: - if always_require_approvals := tool.approval_mode.get("always_require_approval"): - mcp["require_approval"] = { - "always": {"tool_names": list(always_require_approvals)} - } - if never_require_approvals := tool.approval_mode.get("never_require_approval"): - mcp["require_approval"] = { - "never": {"tool_names": list(never_require_approvals)} - } - response_tools.append(mcp) + response_tools.append(self.get_mcp_tool(tool)) case HostedCodeInterpreterTool(): tool_args: CodeInterpreterContainerCodeInterpreterToolAuto = {"type": "auto"} if tool.inputs: @@ -305,6 +281,27 @@ def _tools_to_response_tools( response_tools.append(tool_dict) return response_tools + def get_mcp_tool(self, tool: HostedMCPTool) -> MutableMapping[str, Any]: + """Get MCP tool from HostedMCPTool.""" + mcp: Mcp = { + "type": "mcp", + "server_label": tool.name.replace(" ", "_"), + "server_url": str(tool.url), + "server_description": tool.description, + "headers": tool.headers, + } + if tool.allowed_tools: + mcp["allowed_tools"] = list(tool.allowed_tools) + if tool.approval_mode: + match tool.approval_mode: + case str(): + mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never" + case _: + if always_require_approvals := tool.approval_mode.get("always_require_approval"): + mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}} + if never_require_approvals := tool.approval_mode.get("never_require_approval"): + mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} + async def prepare_options( self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions ) -> dict[str, Any]: diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index c44737b725..0a8b01d682 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -12,6 +12,7 @@ This folder contains examples demonstrating different ways to create and use age | [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. | | [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Shows how to work with a pre-existing conversation by providing the conversation ID to continue existing chat sessions. | | [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. | +| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. | | [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. | | [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py new file mode 100644 index 0000000000..ab4a1d53ef --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import HostedMCPTool +from agent_framework.azure import AzureAIClient +from azure.identity.aio import AzureCliCredential + +""" +Azure AI Agent with Hosted MCP Example + +This sample demonstrates integrating hosted Model Context Protocol (MCP) tools with Azure AI Agent. +""" + + +async def run_hosted_mcp() -> None: + # Since no Agent ID is provided, the agent will be automatically created. + # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred + # authentication option. + async with ( + AzureCliCredential() as credential, + AzureAIClient(async_credential=credential).create_agent( + name="MyDocsAgent", + instructions="You are a helpful assistant that can help with Microsoft documentation questions.", + tools=HostedMCPTool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # "always_require" mode is not supported yet + approval_mode="never_require", + ), + ) as agent, + ): + query = "How to create an Azure storage account using az cli?" + print(f"User: {query}") + result = await agent.run(query) + print(f"{agent.name}: {result}\n") + + +async def main() -> None: + print("=== Azure AI Agent with Hosted Mcp Tools Example ===\n") + + await run_hosted_mcp() + + +if __name__ == "__main__": + asyncio.run(main())