mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Fix toolbox consent flow in hosted agent
This commit is contained in:
@@ -11,7 +11,7 @@ import tempfile
|
||||
import threading
|
||||
from collections.abc import AsyncIterable, AsyncIterator, Generator, Sequence
|
||||
from contextlib import AbstractAsyncContextManager, AsyncExitStack, suppress
|
||||
from dataclasses import asdict, is_dataclass
|
||||
from dataclasses import asdict, dataclass, is_dataclass
|
||||
from pathlib import Path
|
||||
from typing import Protocol, cast
|
||||
|
||||
@@ -264,10 +264,16 @@ def _checkpoint_storage_for_context(root: str, context_id: str) -> FileCheckpoin
|
||||
|
||||
# Foundry Toolbox Auth integration
|
||||
# Consent-URL error code returned by the Foundry MCP gateway when calling `/list`
|
||||
CONSENT_ERROR_CODE = -32007
|
||||
CONSENT_ERROR_CODE = -32006
|
||||
|
||||
|
||||
def consent_url_from_error(exc: BaseException) -> str | None:
|
||||
@dataclass
|
||||
class ConsentError:
|
||||
name: str
|
||||
consent_url: str
|
||||
|
||||
|
||||
def consent_url_from_error(exc: BaseException) -> list[ConsentError] | None:
|
||||
"""Return the consent URL when ``exc`` wraps a Foundry MCP gateway consent error.
|
||||
|
||||
The Agent Framework MCP layer surfaces gateway consent failures by wrapping the underlying
|
||||
@@ -285,7 +291,53 @@ def consent_url_from_error(exc: BaseException) -> str | None:
|
||||
"""
|
||||
inner_exception = next((arg for arg in exc.args if isinstance(arg, McpError)), None)
|
||||
if inner_exception is not None and inner_exception.error.code == CONSENT_ERROR_CODE:
|
||||
return inner_exception.error.message
|
||||
# Parse the error message
|
||||
# The error message is structured with the following format:
|
||||
# "tools/list failed for 1 tool source(s), succeeded for 0 tool source(s) {"errors":[{"name": ..."
|
||||
# where the second part is a JSON string that can be deserialized into an object with the following shape:
|
||||
# ruff: disable[ERA001]
|
||||
# {
|
||||
# "errors" : [
|
||||
# {
|
||||
# "name": "Name of the MCP tool that requires consent",
|
||||
# "type" : "mcp",
|
||||
# "error": {
|
||||
# "code": "CONSENT_REQUIRED",
|
||||
# "message": consent_url,
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ruff: enable[ERA001]
|
||||
try:
|
||||
consent_errors: list[ConsentError] = []
|
||||
error_message_start = inner_exception.error.message.find("{")
|
||||
if error_message_start == -1:
|
||||
logger.warning("Consent error message does not contain JSON: %s", inner_exception.error.message)
|
||||
return None
|
||||
consent_details_json = inner_exception.error.message[error_message_start:]
|
||||
consent_details = json.loads(consent_details_json)
|
||||
if "errors" not in consent_details or not isinstance(consent_details["errors"], list):
|
||||
logger.warning("Consent error message JSON does not contain 'errors' list: %s", consent_details_json)
|
||||
return None
|
||||
for error in consent_details["errors"]:
|
||||
if (
|
||||
isinstance(error, dict)
|
||||
and error.get("type") == "mcp" # type: ignore
|
||||
and "error" in error
|
||||
and isinstance(error["error"], dict)
|
||||
and error["error"].get("code") == "CONSENT_REQUIRED" # type: ignore
|
||||
and "message" in error["error"]
|
||||
):
|
||||
consent_url = error["error"]["message"] # type: ignore
|
||||
if isinstance(consent_url, str):
|
||||
consent_errors.append(ConsentError(name=error.get("name", "Unknown"), consent_url=consent_url)) # type: ignore
|
||||
else:
|
||||
logger.warning("Consent URL in error message is not a valid URL: %s", consent_url) # type: ignore
|
||||
if consent_errors:
|
||||
return consent_errors
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Failed to parse consent details JSON: %s", inner_exception.error.message)
|
||||
return None
|
||||
|
||||
|
||||
@@ -448,18 +500,19 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
try:
|
||||
await self._ensure_agent_ready()
|
||||
except AgentFrameworkException as ex:
|
||||
consent_url = consent_url_from_error(ex)
|
||||
if consent_url is None:
|
||||
consent_errors = consent_url_from_error(ex)
|
||||
if consent_errors is None:
|
||||
raise
|
||||
logger.warning("OAuth consent required for Foundry MCP gateway.")
|
||||
oauth_item = OAuthConsentRequestOutputItem(
|
||||
id=IdGenerator.new_id("oacr"),
|
||||
consent_link=consent_url,
|
||||
server_label="Foundry Toolbox",
|
||||
)
|
||||
builder = response_event_stream.add_output_item(oauth_item.id)
|
||||
yield builder.emit_added(oauth_item)
|
||||
yield builder.emit_done(oauth_item)
|
||||
for consent_error in consent_errors:
|
||||
logger.warning("Consent URL for tool '%s': %s", consent_error.name, consent_error.consent_url)
|
||||
oauth_item = OAuthConsentRequestOutputItem(
|
||||
id=IdGenerator.new_id("oacr"),
|
||||
consent_link=consent_error.consent_url,
|
||||
server_label=consent_error.name,
|
||||
)
|
||||
builder = response_event_stream.add_output_item(oauth_item.id)
|
||||
yield builder.emit_added(oauth_item)
|
||||
yield builder.emit_done(oauth_item)
|
||||
yield response_event_stream.emit_completed()
|
||||
return
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ You can connect to MCP servers in Foundry Toolbox that use different authenticat
|
||||
- **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.
|
||||
> Definitions of these authentication methods can be found in the [agent.manifest.yaml](agent.manifest.yaml) file in this sample. The GitHub MCP connection is defaulted to used PAT for authentication in this sample, but you can switch to OAuth2 by changing the `project_connection_id` field in the `agent.manifest.yaml` file and following the instructions in the comments.
|
||||
|
||||
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/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md).
|
||||
|
||||
|
||||
+73
-73
@@ -18,57 +18,57 @@ template:
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: TOOLBOX_NAME
|
||||
value: "agent-tools-2"
|
||||
# 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
|
||||
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
|
||||
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
|
||||
# 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
|
||||
@@ -81,29 +81,29 @@ resources:
|
||||
# # 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"
|
||||
- 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"
|
||||
|
||||
+2
-3
@@ -1,4 +1,3 @@
|
||||
# agent-framework
|
||||
# agent-framework-foundry-hosting
|
||||
|
||||
agent-framework
|
||||
agent-framework-foundry-hosting
|
||||
mcp>=1.24.0,<2
|
||||
|
||||
Generated
+3952
-3971
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user