From 31aeb354c06ba5632574c2d14ee3ed50a8795462 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Wed, 3 Dec 2025 14:29:03 -0800 Subject: [PATCH 1/4] added azure ai local mcp sample --- .../azure_ai/azure_ai_with_local_mcp.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py new file mode 100644 index 0000000000..801e26ea01 --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import MCPStreamableHTTPTool +from agent_framework.azure import AzureAIClient +from azure.identity.aio import AzureCliCredential + +""" +Azure AI Agent With Local MCP Example + +This sample demonstrates integration of Azure AI Agents with local Model Context Protocol (MCP) +servers, showing both agent-level and run-level tool configuration patterns. + +Pre-requisites: +- Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME + environment variables before running this sample. +""" + + +async def mcp_tools_on_agent_level() -> None: + """Example showing MCP tools defined when creating the agent.""" + print("=== Tools Defined on Agent Level ===") + + # Tools are provided when creating the agent + # The agent can use these tools for any query during its lifetime + # The agent will connect to the MCP server through its context manager. + async with ( + AzureCliCredential() as credential, + AzureAIClient(async_credential=credential).create_agent( + name="DocsAgent", + instructions="You are a helpful assistant that can help with microsoft documentation questions.", + tools=MCPStreamableHTTPTool( # Tools defined at agent creation + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ), + ) as 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}\n") + 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}\n") + + +async def mcp_tools_on_run_level() -> None: + """Example showing MCP tools defined when running the agent.""" + print("=== Tools Defined on Run Level ===") + + # Tools are provided when running the agent + # This means we have to ensure we connect to the MCP server before running the agent + # and pass the tools to the run method. + async with ( + AzureCliCredential() as credential, + MCPStreamableHTTPTool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ) as mcp_server, + AzureAIClient(async_credential=credential).create_agent( + name="DocsAgent", + instructions="You are a helpful assistant that can help with microsoft documentation questions.", + ) as 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, tools=mcp_server) + print(f"Agent: {first_result}\n") + print("\n=======================================\n") + # Second query + second_query = "What is Microsoft Agent Framework?" + print(f"User: {second_query}") + second_result = await agent.run(second_query, tools=mcp_server) + print(f"Agent: {second_result}\n") + + +async def main() -> None: + print("=== Azure AI Agent with Local MCP Tools Example ===\n") + + await mcp_tools_on_agent_level() + await mcp_tools_on_run_level() + + +if __name__ == "__main__": + asyncio.run(main()) From a1ae2e6879197dadb1fdf89c27a2d5c983f67483 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Wed, 3 Dec 2025 14:51:42 -0800 Subject: [PATCH 2/4] small fix --- .../agents/azure_ai/azure_ai_with_local_mcp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py index 801e26ea01..890c51c31a 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py @@ -7,7 +7,7 @@ from azure.identity.aio import AzureCliCredential """ -Azure AI Agent With Local MCP Example +Azure AI Agent with Local MCP Example This sample demonstrates integration of Azure AI Agents with local Model Context Protocol (MCP) servers, showing both agent-level and run-level tool configuration patterns. @@ -29,7 +29,7 @@ async def mcp_tools_on_agent_level() -> None: AzureCliCredential() as credential, AzureAIClient(async_credential=credential).create_agent( name="DocsAgent", - instructions="You are a helpful assistant that can help with microsoft documentation questions.", + instructions="You are a helpful assistant that can help with Microsoft documentation questions.", tools=MCPStreamableHTTPTool( # Tools defined at agent creation name="Microsoft Learn MCP", url="https://learn.microsoft.com/api/mcp", @@ -64,7 +64,7 @@ async def mcp_tools_on_run_level() -> None: ) as mcp_server, AzureAIClient(async_credential=credential).create_agent( name="DocsAgent", - instructions="You are a helpful assistant that can help with microsoft documentation questions.", + instructions="You are a helpful assistant that can help with Microsoft documentation questions.", ) as agent, ): # First query From d9f7ba86621bd9bb171c2176afe1ddfd09d795c3 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Wed, 3 Dec 2025 19:12:26 -0800 Subject: [PATCH 3/4] handling for local mcp --- .../openai/_responses_client.py | 14 +++++ .../openai/test_openai_responses_client.py | 58 +++++++++++++++++++ .../getting_started/agents/azure_ai/README.md | 1 + .../azure_ai/azure_ai_with_local_mcp.py | 55 +++--------------- 4 files changed, 80 insertions(+), 48 deletions(-) diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index 6d4fce7bb2..6b4222062e 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -26,6 +26,7 @@ from .._clients import BaseChatClient from .._logging import get_logger +from .._mcp import MCPTool from .._middleware import use_chat_middleware from .._tools import ( AIFunction, @@ -327,6 +328,19 @@ def _tools_to_response_tools( else None, ) ) + case MCPTool(): + for func in tool.functions: + params = func.parameters() + params["additionalProperties"] = False + response_tools.append( + FunctionToolParam( + name=func.name, + parameters=params, + strict=False, + type="function", + description=func.description, + ) + ) case _: logger.debug("Unsupported tool passed (type: %s)", type(tool)) else: diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index c4d824d31d..1254d3d8ba 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -653,6 +653,64 @@ def test_tools_to_response_tools_with_hosted_mcp() -> None: assert "require_approval" in mcp +def test_tools_to_response_tools_with_local_mcp() -> None: + """Test that local MCPTool (MCPStreamableHTTPTool) functions are converted to FunctionToolParam.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + + # Create a mock MCPStreamableHTTPTool with mock functions + mcp_tool = MCPStreamableHTTPTool( + name="test-mcp", + url="https://example.com/mcp", + description="Test MCP tool", + ) + + # Mock the functions property to return AIFunction instances + mock_func1 = MagicMock() + mock_func1.name = "search_docs" + mock_func1.description = "Search documentation" + mock_func1.parameters.return_value = { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + } + + mock_func2 = MagicMock() + mock_func2.name = "fetch_page" + mock_func2.description = "Fetch a page" + mock_func2.parameters.return_value = { + "type": "object", + "properties": {"url": {"type": "string"}}, + "required": ["url"], + } + + # Mock the functions property + with patch.object( + type(mcp_tool), "functions", new_callable=lambda: property(lambda self: [mock_func1, mock_func2]) + ): + resp_tools = client._tools_to_response_tools([mcp_tool]) + + assert isinstance(resp_tools, list) + assert len(resp_tools) == 2 + + # Verify first function + func1 = resp_tools[0] + assert func1["type"] == "function" + assert func1["name"] == "search_docs" + assert func1["description"] == "Search documentation" + assert func1["strict"] is False + assert func1["parameters"]["additionalProperties"] is False + assert "query" in func1["parameters"]["properties"] + + # Verify second function + func2 = resp_tools[1] + assert func2["type"] == "function" + assert func2["name"] == "fetch_page" + assert func2["description"] == "Fetch a page" + assert func2["strict"] is False + assert func2["parameters"]["additionalProperties"] is False + assert "url" in func2["parameters"]["properties"] + + def test_create_response_content_with_mcp_approval_request() -> None: """Test that a non-streaming mcp_approval_request is parsed into FunctionApprovalRequestContent.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index 17a944524a..437094795b 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -20,6 +20,7 @@ This folder contains examples demonstrating different ways to create and use age | [`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_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. | | [`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_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate local Model Context Protocol (MCP) tools with Azure AI agents. | | [`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_runtime_json_schema.py`](azure_ai_with_runtime_json_schema.py) | Shows how to use structured outputs (response format) with Azure AI agents using a JSON schema to enforce specific response schemas. | | [`azure_ai_with_search_context_agentic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py) | Shows how to use AzureAISearchContextProvider with agentic mode. Uses Knowledge Bases for multi-hop reasoning across documents with query planning. Recommended for most scenarios - slightly slower with more token consumption for query planning, but more accurate results. | diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py index 890c51c31a..dd8cb0abab 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py @@ -10,7 +10,7 @@ Azure AI Agent with Local MCP Example This sample demonstrates integration of Azure AI Agents with local Model Context Protocol (MCP) -servers, showing both agent-level and run-level tool configuration patterns. +servers. Pre-requisites: - Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME @@ -18,19 +18,16 @@ """ -async def mcp_tools_on_agent_level() -> None: - """Example showing MCP tools defined when creating the agent.""" - print("=== Tools Defined on Agent Level ===") +async def main() -> None: + """Example showing use of Local MCP Tool with AzureAIClient.""" + print("=== Azure AI Agent with Local MCP Tools Example ===\n") - # Tools are provided when creating the agent - # The agent can use these tools for any query during its lifetime - # The agent will connect to the MCP server through its context manager. async with ( AzureCliCredential() as credential, AzureAIClient(async_credential=credential).create_agent( name="DocsAgent", instructions="You are a helpful assistant that can help with Microsoft documentation questions.", - tools=MCPStreamableHTTPTool( # Tools defined at agent creation + tools=MCPStreamableHTTPTool( name="Microsoft Learn MCP", url="https://learn.microsoft.com/api/mcp", ), @@ -40,51 +37,13 @@ async def mcp_tools_on_agent_level() -> None: 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}\n") + 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}\n") - - -async def mcp_tools_on_run_level() -> None: - """Example showing MCP tools defined when running the agent.""" - print("=== Tools Defined on Run Level ===") - - # Tools are provided when running the agent - # This means we have to ensure we connect to the MCP server before running the agent - # and pass the tools to the run method. - async with ( - AzureCliCredential() as credential, - MCPStreamableHTTPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ) as mcp_server, - AzureAIClient(async_credential=credential).create_agent( - name="DocsAgent", - instructions="You are a helpful assistant that can help with Microsoft documentation questions.", - ) as 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, tools=mcp_server) - print(f"Agent: {first_result}\n") - print("\n=======================================\n") - # Second query - second_query = "What is Microsoft Agent Framework?" - print(f"User: {second_query}") - second_result = await agent.run(second_query, tools=mcp_server) - print(f"Agent: {second_result}\n") - - -async def main() -> None: - print("=== Azure AI Agent with Local MCP Tools Example ===\n") - - await mcp_tools_on_agent_level() - await mcp_tools_on_run_level() + print(f"Agent: {second_result}") if __name__ == "__main__": From c89fb927a242c71beac99c69b3e22006d9fdd925 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Thu, 4 Dec 2025 10:20:11 -0800 Subject: [PATCH 4/4] remove redundant local mcp handling --- .../openai/_responses_client.py | 14 ----- .../openai/test_openai_responses_client.py | 58 ------------------- 2 files changed, 72 deletions(-) diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index 6b4222062e..6d4fce7bb2 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -26,7 +26,6 @@ from .._clients import BaseChatClient from .._logging import get_logger -from .._mcp import MCPTool from .._middleware import use_chat_middleware from .._tools import ( AIFunction, @@ -328,19 +327,6 @@ def _tools_to_response_tools( else None, ) ) - case MCPTool(): - for func in tool.functions: - params = func.parameters() - params["additionalProperties"] = False - response_tools.append( - FunctionToolParam( - name=func.name, - parameters=params, - strict=False, - type="function", - description=func.description, - ) - ) case _: logger.debug("Unsupported tool passed (type: %s)", type(tool)) else: diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index 1254d3d8ba..c4d824d31d 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -653,64 +653,6 @@ def test_tools_to_response_tools_with_hosted_mcp() -> None: assert "require_approval" in mcp -def test_tools_to_response_tools_with_local_mcp() -> None: - """Test that local MCPTool (MCPStreamableHTTPTool) functions are converted to FunctionToolParam.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - - # Create a mock MCPStreamableHTTPTool with mock functions - mcp_tool = MCPStreamableHTTPTool( - name="test-mcp", - url="https://example.com/mcp", - description="Test MCP tool", - ) - - # Mock the functions property to return AIFunction instances - mock_func1 = MagicMock() - mock_func1.name = "search_docs" - mock_func1.description = "Search documentation" - mock_func1.parameters.return_value = { - "type": "object", - "properties": {"query": {"type": "string"}}, - "required": ["query"], - } - - mock_func2 = MagicMock() - mock_func2.name = "fetch_page" - mock_func2.description = "Fetch a page" - mock_func2.parameters.return_value = { - "type": "object", - "properties": {"url": {"type": "string"}}, - "required": ["url"], - } - - # Mock the functions property - with patch.object( - type(mcp_tool), "functions", new_callable=lambda: property(lambda self: [mock_func1, mock_func2]) - ): - resp_tools = client._tools_to_response_tools([mcp_tool]) - - assert isinstance(resp_tools, list) - assert len(resp_tools) == 2 - - # Verify first function - func1 = resp_tools[0] - assert func1["type"] == "function" - assert func1["name"] == "search_docs" - assert func1["description"] == "Search documentation" - assert func1["strict"] is False - assert func1["parameters"]["additionalProperties"] is False - assert "query" in func1["parameters"]["properties"] - - # Verify second function - func2 = resp_tools[1] - assert func2["type"] == "function" - assert func2["name"] == "fetch_page" - assert func2["description"] == "Fetch a page" - assert func2["strict"] is False - assert func2["parameters"]["additionalProperties"] is False - assert "url" in func2["parameters"]["properties"] - - def test_create_response_content_with_mcp_approval_request() -> None: """Test that a non-streaming mcp_approval_request is parsed into FunctionApprovalRequestContent.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")