Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/ai/azure-ai-projects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions sdk/ai/azure-ai-projects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion sdk/ai/azure-ai-projects/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# pylint: disable=line-too-long,useless-suppression
Comment thread
howieleung marked this conversation as resolved.
# ------------------------------------
# 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.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you not also call this as part of cleanup at the bottom? Even though you do it at the start of the sample

project_client.beta.toolboxes.delete(TOOLBOX_NAME)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now took out toolbox deletion from the top.
Do you mean should not clean up agents?
After developing many samples, I have a preference to keep samples simple to improve readability. So I now prefer not to delete agent. But because we have been deleting agents at the end of each sample, I just keep agent deletion here for consistency.

@dargilco dargilco May 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to decide if we do cleanup or not. But we can't do "half cleanup"... Until now we always did full cleanup at the end of the sample, so I prefer we continue doing that until/if we make another decision. We can discuss this in the sync meeting.

Original file line number Diff line number Diff line change
@@ -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())
18 changes: 18 additions & 0 deletions sdk/ai/azure-ai-projects/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<x>, /projects/<x>, 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.
Expand Down