diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 1caa7949ab75..c7f1b5a00e9a 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -39,6 +39,7 @@ Breaking changes in beta classes: * Added Hosted Agent creation sample `sample_hosted_agent_create.py`, demonstrating hosted agent version creation and retrieval with `AIProjectClient`. * The Hosted Agent creation sample also demonstrates assigning the hosted agent managed identity the Azure AI User RBAC role on the backing Azure AI account. * Updated the other Hosted Agent samples to reuse an existing Hosted Agent as a prerequisite, instead of creating a new hosted agent version in each sample. +* Added Toolbox tool-search sample `sample_toolboxes_with_search_preview.py` and `sample_toolboxes_with_search_preview_async.py`, demonstrating creating a Toolbox version with `ToolboxSearchPreviewTool` and invoking `MCPTool`. ## 2.1.0 (2026-04-20) diff --git a/sdk/ai/azure-ai-projects/README.md b/sdk/ai/azure-ai-projects/README.md index a442f76d91fc..39d57c2b21f3 100644 --- a/sdk/ai/azure-ai-projects/README.md +++ b/sdk/ai/azure-ai-projects/README.md @@ -22,6 +22,7 @@ resources in your Microsoft Foundry Project. Use it to: * Microsoft SharePoint (Preview) * Model Context Protocol (MCP) * OpenAPI + * Toolbox Search (Preview) * Web Search * Web Search (Preview) * Work IQ (Preview) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 59aa2d33f39a..8d9d9578ab7d 100644 --- a/sdk/ai/azure-ai-projects/assets.json +++ b/sdk/ai/azure-ai-projects/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/ai/azure-ai-projects", - "Tag": "python/ai/azure-ai-projects_f15b61b44c" + "Tag": "python/ai/azure-ai-projects_078d4bf75b" } diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview.py new file mode 100644 index 000000000000..63bdc57e0ee8 --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview.py @@ -0,0 +1,125 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + This sample demonstrates how to create a Toolbox in tool-search mode and + invoke it from a Prompt Agent using the synchronous AIProjectClient and + the OpenAI-compatible client. + + A toolbox version that includes 'ToolboxSearchPreviewTool' exposes only + two meta tools at its '/mcp' endpoint -- 'tool_search' and 'call_tool' + -- and defers every other tool behind them. The agent uses an 'MCPTool' + pointed at the toolbox's versioned '/mcp' URL to discover and invoke + those inner tools. + + Toolboxes and tool search are preview features. CRUD goes through + 'project_client.beta.toolboxes'. + +USAGE: + python sample_toolboxes_with_search_preview.py + + Before running the sample: + + pip install "azure-ai-projects>=2.2.0" python-dotenv openai + + Set these environment variables with your own values: + 1) FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint, as found in the Overview + page of your Microsoft Foundry portal. + 2) FOUNDRY_MODEL_NAME - The deployment name of the AI model, as found under the "Name" column in + the "Models + endpoints" tab in your Microsoft Foundry project. + 3) MCP_PROJECT_CONNECTION_ID - The connection resource ID in Custom keys used by + the inner MCP server inside the toolbox. +""" + +import os + +from dotenv import load_dotenv + +from azure.core.exceptions import ResourceNotFoundError +from azure.identity import DefaultAzureCredential + +from azure.ai.projects import AIProjectClient +from azure.ai.projects.models import ( + MCPTool, + PromptAgentDefinition, + ToolboxSearchPreviewTool, +) + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + +TOOLBOX_NAME = "toolbox_with_mcp_tool" +INNER_MCP_LABEL = "github" +INNER_MCP_URL = "https://api.githubcopilot.com/mcp" +TOOLBOX_MCP_LABEL = "search-tool" + + +with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential) as project_client, + project_client.get_openai_client() as openai_client, +): + + inner_mcp_tool = MCPTool( + server_label=INNER_MCP_LABEL, + server_url=INNER_MCP_URL, + require_approval="never", + project_connection_id=os.environ["MCP_PROJECT_CONNECTION_ID"], + ) + + toolbox_version = project_client.beta.toolboxes.create_version( + name=TOOLBOX_NAME, + description=f"Toolbox with `{INNER_MCP_LABEL}` MCP server and tool search enabled.", + tools=[inner_mcp_tool, ToolboxSearchPreviewTool()], + ) + print(f"Created toolbox `{TOOLBOX_NAME}` (version {toolbox_version.version}).") + + toolbox_mcp_url = f"{endpoint}/toolboxes/{TOOLBOX_NAME}/versions/{toolbox_version.version}/mcp?api-version=v1" + token = credential.get_token("https://ai.azure.com/.default").token + + toolbox_mcp_tool = MCPTool( + server_label=TOOLBOX_MCP_LABEL, + server_url=toolbox_mcp_url, + authorization=token, + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + require_approval="never", + ) + + agent = project_client.agents.create_version( + agent_name="MyAgent", + definition=PromptAgentDefinition( + model=os.environ["FOUNDRY_MODEL_NAME"], + instructions=( + "Always use the toolbox search tool to answer questions and perform tasks. " + "Use `tool_search` to discover a relevant tool, then `call_tool` " + "with the tool name returned by the search." + ), + tools=[toolbox_mcp_tool], + ), + ) + print(f"Agent created (id: {agent.id}, name: {agent.name}, version: {agent.version}).") + + response = openai_client.responses.create( + input="What is my username in Github profile?", + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + + for item in response.output: + if item.type == "mcp_approval_request": + print(f"server_label={item.server_label}, name={item.name}") + elif item.type == "mcp_list_tools": + print(f"server_label={item.server_label}, tools={[t.name for t in (item.tools or [])]}") + elif item.type == "mcp_call": + print(f"server_label={item.server_label}, name={item.name}, error={item.error}") + else: + print() + + print(f"Response: {response.output_text}") + + project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + print(f"Agent version {agent.version} deleted.") diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview_async.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview_async.py new file mode 100644 index 000000000000..bfd43071c2f3 --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview_async.py @@ -0,0 +1,130 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + This sample demonstrates how to create a Toolbox in tool-search mode and + invoke it from a Prompt Agent using the asynchronous AIProjectClient and + the OpenAI-compatible client. + + A toolbox version that includes 'ToolboxSearchPreviewTool' exposes only + two meta tools at its '/mcp' endpoint -- 'tool_search' and 'call_tool' + -- and defers every other tool behind them. The agent uses an 'MCPTool' + pointed at the toolbox's versioned '/mcp' URL to discover and invoke + those inner tools. + + Toolboxes and tool search are preview features. CRUD goes through + 'project_client.beta.toolboxes'. + +USAGE: + python sample_toolboxes_with_search_preview_async.py + + Before running the sample: + + pip install "azure-ai-projects>=2.2.0" python-dotenv openai aiohttp + + Set these environment variables with your own values: + 1) FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint, as found in the Overview + page of your Microsoft Foundry portal. + 2) FOUNDRY_MODEL_NAME - The deployment name of the AI model, as found under the "Name" column in + the "Models + endpoints" tab in your Microsoft Foundry project. + 3) MCP_PROJECT_CONNECTION_ID - The connection resource ID in Custom keys used by + the inner MCP server inside the toolbox. +""" + +import asyncio +import os + +from dotenv import load_dotenv + +from azure.identity.aio import DefaultAzureCredential + +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import ( + MCPTool, + PromptAgentDefinition, + ToolboxSearchPreviewTool, +) + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + +TOOLBOX_NAME = "toolbox_with_mcp_tool" +INNER_MCP_LABEL = "github" +INNER_MCP_URL = "https://api.githubcopilot.com/mcp" +TOOLBOX_MCP_LABEL = "search-tool" + + +async def main() -> None: + async with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential) as project_client, + project_client.get_openai_client() as openai_client, + ): + + inner_mcp_tool = MCPTool( + server_label=INNER_MCP_LABEL, + server_url=INNER_MCP_URL, + require_approval="never", + project_connection_id=os.environ["MCP_PROJECT_CONNECTION_ID"], + ) + + toolbox_version = await project_client.beta.toolboxes.create_version( + name=TOOLBOX_NAME, + description=f"Toolbox with `{INNER_MCP_LABEL}` MCP server and tool search enabled.", + tools=[inner_mcp_tool, ToolboxSearchPreviewTool()], + ) + print(f"Created toolbox `{TOOLBOX_NAME}` (version {toolbox_version.version}).") + + toolbox_mcp_url = f"{endpoint}/toolboxes/{TOOLBOX_NAME}/versions/{toolbox_version.version}/mcp?api-version=v1" + token = (await credential.get_token("https://ai.azure.com/.default")).token + + toolbox_mcp_tool = MCPTool( + server_label=TOOLBOX_MCP_LABEL, + server_url=toolbox_mcp_url, + authorization=token, + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + require_approval="never", + ) + + agent = await project_client.agents.create_version( + agent_name="MyAgent", + definition=PromptAgentDefinition( + model=os.environ["FOUNDRY_MODEL_NAME"], + instructions=( + "Always use the toolbox search tool to answer questions and perform tasks. " + "Use `tool_search` to discover a relevant tool, then `call_tool` " + "with the tool name returned by the search." + ), + tools=[toolbox_mcp_tool], + ), + ) + print(f"Agent created (id: {agent.id}, name: {agent.name}, version: {agent.version}).") + + response = await openai_client.responses.create( + input="What is my username in Github profile?", + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + + for item in response.output: + if item.type == "mcp_approval_request": + print(f"server_label={item.server_label}, name={item.name}") + elif item.type == "mcp_list_tools": + print(f"server_label={item.server_label}, tools={[t.name for t in (item.tools or [])]}") + elif item.type == "mcp_call": + print(f"server_label={item.server_label}, name={item.name}, error={item.error}") + else: + print() + + print(f"Response: {response.output_text}") + + await project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + print(f"Agent version {agent.version} deleted.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/ai/azure-ai-projects/tests/conftest.py b/sdk/ai/azure-ai-projects/tests/conftest.py index a6dd76036377..e75608bf08ed 100644 --- a/sdk/ai/azure-ai-projects/tests/conftest.py +++ b/sdk/ai/azure-ai-projects/tests/conftest.py @@ -181,6 +181,24 @@ def sanitize_url_paths(): regex=r"(?i)^Bearer\s+github_pat_[A-Za-z0-9_]+$", ) + # Sanitize raw Entra-ID JWTs (no "Bearer " prefix) passed via MCPTool.authorization + # to match the `fake_token` value the FakeTokenCredential returns during playback. + add_body_key_sanitizer( + json_path="$..authorization", + value="fake_token", + regex=r"^eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+$", + ) + + # Sanitize Cognitive Services / Foundry account hostnames inside request and + # response bodies (e.g. MCPTool.server_url built from FOUNDRY_PROJECT_ENDPOINT). + # URL-path sanitizers above already redact /accounts/, /projects/, etc., + # but the host is built into body fields and needs its own redaction so + # recordings match the playback FOUNDRY_PROJECT_ENDPOINT. + add_body_regex_sanitizer( + regex=r"https://[a-z0-9-]+\.services\.ai\.azure\.com", + value=f"https://{SanitizedValues.ACCOUNT_NAME}.services.ai.azure.com", + ) + # Sanitize Azure Blob account host while preserving container path and SAS shape. # This avoids creating inconsistent recordings where sasUri points to a different # container than the corresponding blob RequestUri entries.