Added hosted MCP support (#2018)

This commit is contained in:
Dmytro Struk
2025-11-07 13:55:21 -08:00
committed by GitHub
Unverified
parent cfcfd713d2
commit 476fbbefc3
4 changed files with 96 additions and 26 deletions
@@ -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.observability import use_observability
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 @@ class AzureAIClient(OpenAIBaseResponsesClient):
# 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
@@ -187,31 +187,7 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
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 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
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]:
@@ -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. |
@@ -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())