From 5b59954b48a06aac9933fb0dfc9c8fe30a8d4f51 Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Fri, 8 May 2026 11:42:05 -0700 Subject: [PATCH] Show more authentication methods in Foundry Toolbox MCP --- .../responses/04_foundry_toolbox/README.md | 14 ++++ .../04_foundry_toolbox/agent.manifest.yaml | 75 ++++++++++++++++++- .../responses/04_foundry_toolbox/main.py | 49 ++++++------ .../responses/06_files/README.md | 5 +- .../responses/06_files/main.py | 55 +++++++------- 5 files changed, 147 insertions(+), 51 deletions(-) diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/README.md index bd261061c6..3c043d9a8f 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/README.md @@ -10,6 +10,20 @@ You can also create a Foundry Toolbox in the Foundry portal. Read more about it > If you set up a project with this sample and provision the resources using `azd provision`, a Foundry Toolbox will be created with the specified tools in [`agent.manifest.yaml`](agent.manifest.yaml). +### Authentication Methods + +You can connect to MCP servers in Foundry Toolbox that use different authentication methods. This sample demonstrates the following authentication methods: + +- **No authentication**: The tool does not require any authentication. The agent can invoke the tool without providing any credentials. Sample MCP server: `https://gitmcp.io/Azure/azure-rest-api-specs` +- **Key-based authentication**: The tool requires a key to authenticate. Sample MCP server: `https://api.githubcopilot.com/mcp` (GitHub MCP server) with a Personal Access Token (PAT) for authentication. +- **OAuth2 authentication (managed)**: The tool requires OAuth2 to authenticate. Sample MCP server: `https://api.githubcopilot.com/mcp` (GitHub MCP server) with OAuth2 for authentication. +- **Agent identity authentication**: The tool requires an agent identity token to authenticate. Sample MCP server: `https://{foundry-resource-name}.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview` (Azure Language MCP server) with agent identity for authentication. +- **Entra Pass-through authentication**: The tool requires an Entra pass-through token to authenticate. Sample MCP server: Microsoft Outlook MCP server with Entra pass-through for authentication. + +> Definitions of these authentication methods can be found in the [agent.manifest.yaml](agent.manifest.yaml) file in this sample. + +There are also Non-MCP tools in the toolbox that support different authentication methods. Learn more at the [Foundry sample repository](https://github.com/microsoft-foundry/foundry-samples/tree/main/samples/python/toolbox/azd#supported-scenarios). + ## How It Works ### Model Integration diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/agent.manifest.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/agent.manifest.yaml index c6df32950b..a25c1b59e5 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/agent.manifest.yaml +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/agent.manifest.yaml @@ -19,10 +19,65 @@ template: value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" - name: TOOLBOX_NAME value: "agent-tools" +parameters: + properties: + - name: mcp_endpoint + # `azd ai agent init -m` will prompt for this value when initializing the agent manifest + secret: false + description: URL of the public MCP server (e.g. https://gitmcp.io/Azure/azure-rest-api-specs) that does not require authentication + - name: github_pat + # `azd ai agent init -m` will prompt for this value when initializing the agent manifest + secret: true + description: GitHub Personal Access Token used to authenticate with the GitHub MCP server (press Enter if OAuth2 is used instead) + - name: language_mcp_entra_audience + secret: false + description: Entra ID audience for the Azure Language MCP server (e.g. https://cognitiveservices.azure.com/) + - name: language_mcp_target_url + secret: false + description: URL of the Azure Language MCP server that accepts agent identity tokens (e.g. https://{foundry-resource-name}.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview) + - name: outlook_mail_entra_audience + secret: false + description: Entra ID audience for the Outlook Mail MCP server + - name: outlook_mail_entra_mcp_target + secret: false + description: URL of the Outlook Mail MCP server that accepts user Entra tokens resources: - kind: model id: gpt-4.1-mini name: AZURE_AI_MODEL_DEPLOYMENT_NAME + - kind: connection + # A connection that uses a GitHub Personal Access Token (PAT) to authenticate with the GitHub MCP server + name: github-mcp-pat-conn + category: RemoteTool + authType: CustomKeys + target: https://api.githubcopilot.com/mcp + credentials: + type: CustomKeys + keys: + Authorization: "Bearer {{ github_pat }}" + - kind: connection + # A connection that uses OAuth2 to authenticate with the GitHub MCP server + name: github-mcp-oauth-conn + category: RemoteTool + authType: OAuth2 + target: https://api.githubcopilot.com/mcp + connectorName: foundrygithubmcp + credentials: + type: OAuth2 + clientId: managed + clientSecret: managed + - kind: connection + name: language-mcp-conn + category: RemoteTool + authType: AgenticIdentity + audience: "{{ language_mcp_entra_audience }}" + target: "{{ language_mcp_target_url }}" + - kind: connection + name: outlook-mail-conn + category: RemoteTool + authType: UserEntraToken + audience: "{{ outlook_mail_entra_audience }}" + target: "{{ outlook_mail_entra_mcp_target }}" - kind: toolbox name: agent-tools tools: @@ -30,4 +85,22 @@ resources: name: web_search - type: code_interpreter name: code_interpreter - + - type: mcp + # This MCP tool doesn't require authentication + server_label: noauth_mcp + server_url: "{{ mcp_endpoint }}" + require_approval: "never" + - type: mcp + # This MCP tool uses the GitHub MCP server with a PAT for authentication or OAuth2 + server_label: github + project_connection_id: github-mcp-pat-conn # use `github-mcp-oauth-conn` for OAuth2 authentication + require_approval: "never" + - type: mcp + # This MCP tool uses the Azure Language MCP server with agent identity for authentication + server_label: language-mcp + project_connection_id: language-mcp-conn + require_approval: "never" + - type: mcp + server_label: outlook-mail + project_connection_id: outlook-mail-conn + require_approval: "never" \ No newline at end of file diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/main.py index c836ae0ec1..fdae7faa35 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/main.py +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/main.py @@ -3,12 +3,11 @@ import asyncio import os from collections.abc import Callable -from typing import Any +import httpx from agent_framework import Agent, MCPStreamableHTTPTool from agent_framework.foundry import FoundryChatClient from agent_framework_foundry_hosting import ResponsesHostServer -from azure.core.credentials import TokenCredential from azure.identity import DefaultAzureCredential, get_bearer_token_provider from dotenv import load_dotenv @@ -16,7 +15,7 @@ from dotenv import load_dotenv load_dotenv() -def _resolve_toolbox_endpoint() -> str: +def resolve_toolbox_endpoint() -> str: """Resolve the toolbox MCP endpoint URL. Prefers the explicit ``FOUNDRY_TOOLBOX_ENDPOINT`` env var; falls back to @@ -29,42 +28,50 @@ def _resolve_toolbox_endpoint() -> str: return endpoint project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/") toolbox_name = os.environ["TOOLBOX_NAME"] - return f"{project_endpoint}/toolsets/{toolbox_name}/mcp?api-version=v1" + return f"{project_endpoint}/toolboxes/{toolbox_name}/versions/29/mcp?api-version=v1" -def make_toolbox_header_provider(credential: TokenCredential) -> Callable[[dict[str, Any]], dict[str, str]]: - """Build a header_provider that injects a fresh Azure AI bearer token on every MCP request.""" - get_token = get_bearer_token_provider(credential, "https://ai.azure.com/.default") +class ToolboxAuth(httpx.Auth): + """Injects a fresh bearer token on every request.""" - def provide(_kwargs: dict[str, Any]) -> dict[str, str]: - return { - "Authorization": f"Bearer {get_token()}", - } + def __init__(self, token_provider: Callable[[], str]): + self._get_token = token_provider - return provide + def auth_flow(self, request: httpx.Request): + request.headers["Authorization"] = f"Bearer {self._get_token()}" + yield request async def main(): credential = DefaultAzureCredential() + # Create the toolbox + token_provider = get_bearer_token_provider(credential, "https://ai.azure.com/.default") + + http_client = httpx.AsyncClient( + auth=ToolboxAuth(token_provider), + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + timeout=120.0, + ) + + toolbox = MCPStreamableHTTPTool( + name=os.environ.get("TOOLBOX_NAME", "toolbox"), + url=resolve_toolbox_endpoint(), + http_client=http_client, + load_prompts=False, + ) + + # Create the chat client client = FoundryChatClient( project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=credential, ) - toolbox_tool = MCPStreamableHTTPTool( - name="foundry_toolbox", - description="Tools exposed by the configured Foundry toolbox", - url=_resolve_toolbox_endpoint(), - header_provider=make_toolbox_header_provider(credential), - load_prompts=False, - ) - async with Agent( client=client, instructions="You are a friendly assistant. Keep your answers brief.", - tools=toolbox_tool, + tools=toolbox, # History will be managed by the hosting infrastructure, thus there # is no need to store history by the service. Learn more at: # https://developers.openai.com/api/reference/resources/responses/methods/create diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/README.md index d68ddc16c2..0df9b1508a 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/README.md @@ -21,9 +21,10 @@ This agent uses four tools: 1. **Get Current Working Directory Tool (`get_cwd`)** – Returns the current working directory of the agent host process. 2. **List Files Tool (`list_files`)** – Lists the files in a specified directory. 3. **Read File Tool (`read_file`)** – Reads the contents of a specified file. -4. **Code Interpreter Tool (`code_interpreter`)** – Allows the agent to execute Python code in a safe. +4. **Code Interpreter Tool (`code_interpreter`)** – Allows the agent to execute Python code in a safe sandboxed environment. +5. **Web Search Tool (`web_search`)** – Allows the agent to perform web searches using the Bing Search API. -> In this sample, the filesystem tools are function tools defined in Python using the `@tool` decorator from the Agent Framework. The code interpreter tool is a managed tool provided by [Foundry Toolbox](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox). Learn more about foundry toolbox integration with hosted agents with this [sample](../04_foundry_toolbox/). +> In this sample, the filesystem tools are function tools defined in Python using the `@tool` decorator from the Agent Framework. The code interpreter tool and web search tool are managed tools provided by [Foundry Toolbox](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox). Learn more about foundry toolbox integration with hosted agents with this [sample](../04_foundry_toolbox/). ## Running the Agent Host diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/main.py index a324ab16a1..f7ad4eef46 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/main.py +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/main.py @@ -3,12 +3,11 @@ import asyncio import os from collections.abc import Callable -from typing import Any +import httpx from agent_framework import Agent, MCPStreamableHTTPTool, tool from agent_framework.foundry import FoundryChatClient from agent_framework_foundry_hosting import ResponsesHostServer -from azure.core.credentials import TokenCredential from azure.identity import DefaultAzureCredential, get_bearer_token_provider from dotenv import load_dotenv @@ -16,7 +15,7 @@ from dotenv import load_dotenv load_dotenv() -def _resolve_toolbox_endpoint() -> str: +def resolve_toolbox_endpoint() -> str: """Resolve the toolbox MCP endpoint URL. Prefers the explicit ``FOUNDRY_TOOLBOX_ENDPOINT`` env var; falls back to @@ -29,19 +28,18 @@ def _resolve_toolbox_endpoint() -> str: return endpoint project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/") toolbox_name = os.environ["TOOLBOX_NAME"] - return f"{project_endpoint}/toolsets/{toolbox_name}/mcp?api-version=v1" + return f"{project_endpoint}/toolboxes/{toolbox_name}/mcp?api-version=v1" -def make_toolbox_header_provider(credential: TokenCredential) -> Callable[[dict[str, Any]], dict[str, str]]: - """Build a header_provider that injects a fresh Azure AI bearer token on every MCP request.""" - get_token = get_bearer_token_provider(credential, "https://ai.azure.com/.default") +class ToolboxAuth(httpx.Auth): + """Injects a fresh bearer token on every request.""" - def provide(_kwargs: dict[str, Any]) -> dict[str, str]: - return { - "Authorization": f"Bearer {get_token()}", - } + def __init__(self, token_provider: Callable[[], str]): + self._get_token = token_provider - return provide + def auth_flow(self, request: httpx.Request): + request.headers["Authorization"] = f"Bearer {self._get_token()}" + yield request @tool(description="Get the current working directory.", approval_mode="never_require") @@ -75,26 +73,29 @@ def read_file(file_path: str) -> str: async def main(): credential = DefaultAzureCredential() + # Create the toolbox + token_provider = get_bearer_token_provider(credential, "https://ai.azure.com/.default") + + http_client = httpx.AsyncClient( + auth=ToolboxAuth(token_provider), + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + timeout=120.0, + ) + + toolbox = MCPStreamableHTTPTool( + name=os.environ.get("TOOLBOX_NAME", "toolbox"), + url=resolve_toolbox_endpoint(), + http_client=http_client, + load_prompts=False, + ) + + # Create the chat client client = FoundryChatClient( project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=credential, ) - # Connect to the toolbox MCP endpoint and expose only the code_interpreter tool. - # The toolbox deployed has two tools: (see agent.manifest.yaml) - # - `code_interpreter` - # - `web_search` - # We only need the `code_interpreter` tool for this sample. - toolbox_tool = MCPStreamableHTTPTool( - name="foundry_toolbox", - description="Tools exposed by the configured Foundry toolbox", - url=_resolve_toolbox_endpoint(), - header_provider=make_toolbox_header_provider(credential), - load_prompts=False, - allowed_tools=["code_interpreter"], - ) - async with Agent( client=client, instructions=( @@ -102,7 +103,7 @@ async def main(): "Make sure all mathematical calculations are performed using the code interpreter " "instead of mental arithmetic." ), - tools=[get_cwd, list_files, read_file, toolbox_tool], + tools=[get_cwd, list_files, read_file, toolbox], # History will be managed by the hosting infrastructure, thus there # is no need to store history by the service. Learn more at: # https://developers.openai.com/api/reference/resources/responses/methods/create