From 02c5dd950c89a3facef45102cc2c43dc58d22f82 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Wed, 13 May 2026 12:05:48 -0700 Subject: [PATCH 1/3] Add Toolbox tool-search sample and update changelog and README - Introduced `sample_toolboxes_with_search_preview.py` demonstrating Toolbox creation with `ToolboxSearchPreviewTool`. - Updated CHANGELOG.md to include the new sample. - Added Toolbox Search (Preview) to the README.md features list. - Updated assets.json with the correct Tag version. - Enhanced conftest.py to sanitize Entra-ID JWTs and Cognitive Services hostnames. --- sdk/ai/azure-ai-projects/CHANGELOG.md | 1 + sdk/ai/azure-ai-projects/README.md | 1 + sdk/ai/azure-ai-projects/assets.json | 2 +- .../sample_toolboxes_with_search_preview.py | 134 ++++++++++++++++++ sdk/ai/azure-ai-projects/tests/conftest.py | 18 +++ 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview.py diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 1caa7949ab75..e40955689c4a 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`, 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..befc11ce66d0 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_829b5086f9" } 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..a37bb443e10f --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview.py @@ -0,0 +1,134 @@ +# 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 = "api-specs" +INNER_MCP_URL = "https://gitmcp.io/Azure/azure-rest-api-specs" +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"], + ) + + try: + project_client.beta.toolboxes.delete(TOOLBOX_NAME) + print(f"Deleted existing toolbox `{TOOLBOX_NAME}`.") + except ResourceNotFoundError: + print(f"Toolbox `{TOOLBOX_NAME}` does not exist; nothing to delete.") + + 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=( + "Use `tool_search` to find a tool that can fetch the Azure REST API specs README, " + "then use `call_tool` to invoke it and summarize the result." + ), + 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/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. From a4d4e9786c7ca242c26006f99f121b0cb2b18eaf Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Wed, 13 May 2026 13:18:48 -0700 Subject: [PATCH 2/3] resolved comments --- sdk/ai/azure-ai-projects/CHANGELOG.md | 2 +- sdk/ai/azure-ai-projects/assets.json | 2 +- .../sample_toolboxes_with_search_preview.py | 15 +- ...ple_toolboxes_with_search_preview_async.py | 130 ++++++++++++++++++ 4 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_preview_async.py diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index e40955689c4a..c7f1b5a00e9a 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -39,7 +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`, demonstrating creating a Toolbox version with `ToolboxSearchPreviewTool` and invoking `MCPTool`. +* 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/assets.json b/sdk/ai/azure-ai-projects/assets.json index befc11ce66d0..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_829b5086f9" + "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 index a37bb443e10f..b4c600d241bf 100644 --- 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 @@ -54,8 +54,8 @@ endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] TOOLBOX_NAME = "toolbox_with_mcp_tool" -INNER_MCP_LABEL = "api-specs" -INNER_MCP_URL = "https://gitmcp.io/Azure/azure-rest-api-specs" +INNER_MCP_LABEL = "github" +INNER_MCP_URL = "https://api.githubcopilot.com/mcp" TOOLBOX_MCP_LABEL = "search-tool" @@ -72,12 +72,6 @@ project_connection_id=os.environ["MCP_PROJECT_CONNECTION_ID"], ) - try: - project_client.beta.toolboxes.delete(TOOLBOX_NAME) - print(f"Deleted existing toolbox `{TOOLBOX_NAME}`.") - except ResourceNotFoundError: - print(f"Toolbox `{TOOLBOX_NAME}` does not exist; nothing to delete.") - toolbox_version = project_client.beta.toolboxes.create_version( name=TOOLBOX_NAME, description=f"Toolbox with `{INNER_MCP_LABEL}` MCP server and tool search enabled.", @@ -111,10 +105,7 @@ print(f"Agent created (id: {agent.id}, name: {agent.name}, version: {agent.version}).") response = openai_client.responses.create( - input=( - "Use `tool_search` to find a tool that can fetch the Azure REST API specs README, " - "then use `call_tool` to invoke it and summarize the result." - ), + input="What is my username in Github profile?", extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, ) 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..360ec25bd207 --- /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()) From 85afcbf9b766219dc54d9cf2be78448f226aff5c Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Wed, 13 May 2026 13:21:52 -0700 Subject: [PATCH 3/3] resolved comment --- .../tools/sample_toolboxes_with_search_preview.py | 10 +++++----- .../sample_toolboxes_with_search_preview_async.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) 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 index b4c600d241bf..63bdc57e0ee8 100644 --- 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 @@ -10,14 +10,14 @@ 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 + 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``. + 'project_client.beta.toolboxes'. USAGE: python sample_toolboxes_with_search_preview.py 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 index 360ec25bd207..bfd43071c2f3 100644 --- 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 @@ -10,14 +10,14 @@ 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 + 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``. + 'project_client.beta.toolboxes'. USAGE: python sample_toolboxes_with_search_preview_async.py