Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
FOUNDRY_TOOLBOX_ENDPOINT="..."
TOOLBOX_ENDPOINT="..."

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -17,93 +17,14 @@ template:
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
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.
# Only needed when the GitHub MCP connection is configured to use the `github-mcp-pat-conn`
# PAT-based connection below; if you use the `github-mcp-oauth-conn` OAuth2 connection
# instead, you can leave this empty.
secret: true
description: GitHub Personal Access Token used to authenticate with the GitHub MCP server (only needed when using the PAT connection; press Enter if using OAuth2 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
- name: TOOLBOX_ENDPOINT
# Full MCP endpoint of the toolbox the agent consumes. Create the toolbox
# from the bundled toolbox.yaml, then copy the versioned endpoint it prints
# and store it in your azd environment before you run or deploy:
# azd ai toolbox create my-toolbox --from-file ./toolbox.yaml
# azd env set TOOLBOX_ENDPOINT "<endpoint-from-output>"
value: "{{TOOLBOX_ENDPOINT}}"
resources:
- kind: model
id: gpt-4.1-mini
id: gpt-4.1
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:
- type: web_search
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"
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ resources:
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: TOOLBOX_NAME
value: "agent-tools"
- name: TOOLBOX_ENDPOINT
value: ${TOOLBOX_ENDPOINT}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import os
from collections.abc import Callable
from urllib.parse import urlsplit

import httpx
from agent_framework import Agent, MCPStreamableHTTPTool
Expand All @@ -18,19 +19,42 @@
def resolve_toolbox_endpoint() -> str:
"""Resolve the toolbox MCP endpoint URL.

Prefers the explicit ``FOUNDRY_TOOLBOX_ENDPOINT`` env var; falls back to
constructing the URL from ``FOUNDRY_PROJECT_ENDPOINT`` and ``TOOLBOX_NAME``
(the variables injected by the Foundry hosting scaffolding after ``azd provision``).
Prefers the explicit ``TOOLBOX_ENDPOINT`` env var (set in ``agent.yaml`` or
``agent.manifest.yaml`` and via ``azd env set TOOLBOX_ENDPOINT`` after the toolbox
is created); falls back to constructing the URL from ``FOUNDRY_PROJECT_ENDPOINT``
and ``TOOLBOX_NAME``.
"""
if (endpoint := os.environ.get("FOUNDRY_TOOLBOX_ENDPOINT")) is not None:
if (endpoint := os.environ.get("TOOLBOX_ENDPOINT")) is not None:
Comment thread
TaoChenOSU marked this conversation as resolved.
if not endpoint:
raise ValueError("FOUNDRY_TOOLBOX_ENDPOINT is set but empty")
raise ValueError("TOOLBOX_ENDPOINT is set but empty")
return endpoint
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/")
toolbox_name = os.environ["TOOLBOX_NAME"]
try:
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/")
toolbox_name = os.environ["TOOLBOX_NAME"]
except KeyError as e:
raise ValueError(
"Either set TOOLBOX_ENDPOINT, or set both FOUNDRY_PROJECT_ENDPOINT "
"and TOOLBOX_NAME to build the toolbox MCP endpoint."
) from e
return f"{project_endpoint}/toolboxes/{toolbox_name}/mcp?api-version=v1"


def _toolbox_name_from_endpoint(endpoint: str) -> str:
"""Extract the toolbox name from a toolbox MCP endpoint URL.

Handles both the versioned (``.../toolboxes/<name>/versions/<n>/mcp``) and
unversioned (``.../toolboxes/<name>/mcp``) endpoint shapes that Foundry
produces. Falls back to ``"toolbox"`` when the path has no ``toolboxes``
segment.
"""
segments = urlsplit(endpoint).path.split("/")
if "toolboxes" in segments:
idx = segments.index("toolboxes")
if idx + 1 < len(segments) and segments[idx + 1]:
return segments[idx + 1]
return "toolbox"


class ToolboxAuth(httpx.Auth):
"""Injects a fresh bearer token on every request."""

Expand All @@ -48,11 +72,11 @@ async def main():
# Create the toolbox
token_provider = get_bearer_token_provider(credential, "https://ai.azure.com/.default")

# Resolve the endpoint once and derive the tool name from the same source: when
# ``TOOLBOX_NAME`` isn't explicitly set, parse it out of the resolved URL so the
# tool's local name and the upstream toolbox always agree.
# Resolve the endpoint once and derive a friendly tool name from it. When
# ``TOOLBOX_NAME`` isn't set, extract the toolbox name from the URL path so
# the tool's local name matches the upstream toolbox.
toolbox_endpoint = resolve_toolbox_endpoint()
toolbox_name = os.environ.get("TOOLBOX_NAME") or toolbox_endpoint.rsplit("/mcp", 1)[0].rsplit("/", 1)[-1]
toolbox_name = os.environ.get("TOOLBOX_NAME") or _toolbox_name_from_endpoint(toolbox_endpoint)

async with httpx.AsyncClient(
auth=ToolboxAuth(token_provider),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
agent-framework
agent-framework-foundry-hosting
mcp>=1.24.0,<2
agent-framework-foundry
agent-framework-foundry-hosting
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
List Foundry Tools Catalog connectors, or fetch full details for one connector.

.DESCRIPTION
Queries the Azure AI Foundry Tools Catalog (asset-gallery) connectors registry.
- With no -ConnectorName: lists all connectors (name, title, detected auth type).
- With -ConnectorName: prints the full JSON details for that connector.

A bearer token for https://ai.azure.com is required. It is read from the
-Token parameter, then the CATALOG_TOKEN environment variable, and finally
acquired automatically via 'az account get-access-token' (requires 'az login').

.EXAMPLE
./list-foundry-connectors.ps1
Lists all connectors.

.EXAMPLE
./list-foundry-connectors.ps1 -ConnectorName a365outlookmailmcp
Prints full details for the Work IQ Mail MCP connector.

.EXAMPLE
./list-foundry-connectors.ps1 -PageSize 2000
Lists more connectors in a single page.
#>
[CmdletBinding()]
param(
# annotations/name of a connector to fetch full details for. Omit to list all.
[string]$ConnectorName,
# Azure ML region host prefix.
[string]$Region = "eastus",
# Number of results to request in one page.
[int]$PageSize = 100,
# Catalog bearer token (audience https://ai.azure.com). Defaults to $env:CATALOG_TOKEN, else acquired via az.
[string]$Token = $env:CATALOG_TOKEN
)

$ErrorActionPreference = "Stop"

if (-not $Token) {
Write-Verbose "No token supplied; acquiring via 'az account get-access-token'..."
$Token = az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv
}
if (-not $Token) {
throw "Failed to acquire a catalog token. Run 'az login', or pass -Token / set `$env:CATALOG_TOKEN."
}

$uri = "https://$Region.api.azureml.ms/asset-gallery/v1.0/tools"
$headers = @{
"Authorization" = "Bearer $Token"
"Content-Type" = "application/json"
"x-ms-user-agent" = "AzureMachineLearningWorkspacePortal/12.0"
}

$filters = [System.Collections.ArrayList]@(
@{ field = "entityContainerId"; operator = "eq"; values = @("connectors-registry-prod-bl") }
@{ field = "type"; operator = "eq"; values = @("tools") }
@{ field = "kind"; operator = "eq"; values = @("Versioned") }
@{ field = "labels"; operator = "eq"; values = @("latest") }
)
if ($ConnectorName) {
[void]$filters.Add(@{ field = "annotations/name"; operator = "eq"; values = @($ConnectorName) })
}

$body = @{
freeTextSearch = "*"
filters = $filters
includeTotalResultCount = $true
pageSize = $PageSize
skip = 0
} | ConvertTo-Json -Depth 10

# The response can be several MB and may contain a property with an empty-string
# name, so read the raw content and parse with -AsHashtable.
$content = (Invoke-WebRequest -Method Post -Uri $uri -Headers $headers -Body $body).Content
$resp = $content | ConvertFrom-Json -AsHashtable -Depth 100

if ($ConnectorName) {
if ($resp.totalCount -eq 0) {
Write-Warning "No connector found with annotations/name '$ConnectorName'."
return
}
$resp.value | ConvertTo-Json -Depth 100
}
else {
Write-Host "Total connectors: $($resp.totalCount)"
$resp.value | ForEach-Object {
$params = $_.properties.'x-ms-connection-parameters'
$authType = if ($null -eq $params) {
"None"
}
else {
$types = $params.Values | ForEach-Object { $_.type }
if ($types -contains "oauthSetting") { "OAuth2" }
elseif ($types -contains "securestring") { "CustomKeys" }
else { "None" }
}
[pscustomobject]@{
Name = $_.annotations.name
Title = $_.properties.title
Auth = $authType
}
} | Format-Table -AutoSize
}
Loading
Loading