From 20ac21c780f97ad4d050844fe66f21f5a6f31b23 Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Mon, 1 Jun 2026 13:25:54 -0700 Subject: [PATCH] Fix tests --- .../_responses.py | 11 ++---- .../foundry_hosting/tests/test_responses.py | 36 ++++++++++++++++--- .../responses/04_foundry_toolbox/README.md | 2 +- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py index 48dddeff07..ae1160212a 100644 --- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py +++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py @@ -274,20 +274,13 @@ class ConsentError: 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 - ``McpError`` inside an :class:`AgentFrameworkException` (typically a ``ToolExecutionException`` - raised from ``MCPStreamableHTTPTool.__aenter__``). This helper inspects ``exc.args`` for a - wrapped ``McpError`` whose ``error.code`` is :data:`CONSENT_ERROR_CODE`; when found, the - consent link the gateway returned in ``error.message`` is returned. Returns ``None`` for - anything else, so callers can do ``if (url := consent_url_from_error(ex)) is None: raise``. + """Return the consent URLs when ``exc`` wraps Foundry MCP gateway consent errors. Args: exc: The exception to inspect. Returns: - The consent URL if ``exc`` wraps a consent ``McpError``, otherwise ``None``. + The consent URL(s) extracted from the error, or ``None`` if no consent error was found. """ 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: diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py index 9358549a86..5a8833f3df 100644 --- a/python/packages/foundry_hosting/tests/test_responses.py +++ b/python/packages/foundry_hosting/tests/test_responses.py @@ -39,6 +39,7 @@ from agent_framework_foundry_hosting import ResponsesHostServer from agent_framework_foundry_hosting._responses import ( _AZURE_RESPONSES_MESSAGE_ROLE_TYPE, # pyright: ignore[reportPrivateUsage] CONSENT_ERROR_CODE, + ConsentError, FileBasedFunctionApprovalStorage, # pyright: ignore[reportPrivateUsage] InMemoryFunctionApprovalStorage, # pyright: ignore[reportPrivateUsage] _item_to_message, # pyright: ignore[reportPrivateUsage] @@ -3067,7 +3068,10 @@ class TestCheckpointContextPathValidation: # region Agent lifecycle (lazy entry & OAuth consent surfacing) -def _make_consent_error(url: str = "https://consent.example.com/auth") -> Exception: +def _make_consent_error( + url: str = "https://consent.example.com/auth", + name: str = "Foundry Toolbox", +) -> Exception: """Build an exception wrapping a Foundry MCP gateway consent error. Mirrors the real-world wrapping produced by ``MCPStreamableHTTPTool.__aenter__``, @@ -3075,17 +3079,34 @@ def _make_consent_error(url: str = "https://consent.example.com/auth") -> Except ``ToolExecutionException`` (an ``AgentFrameworkException`` subclass) with the original error attached via ``inner_exception``. ``consent_url_from_error`` then finds the wrapped ``McpError`` in ``exc.args``. + + The McpError message uses the structured Foundry MCP gateway format: + a human-readable prefix followed by a JSON document describing each + failed tool source and its consent URL. """ from agent_framework.exceptions import ToolExecutionException - inner = McpError(ErrorData(code=CONSENT_ERROR_CODE, message=url)) + payload = json.dumps({ + "errors": [ + { + "name": name, + "type": "mcp", + "error": { + "code": "CONSENT_REQUIRED", + "message": url, + }, + } + ] + }) + message = f"tools/list failed for 1 tool source(s), succeeded for 0 tool source(s) {payload}" + inner = McpError(ErrorData(code=CONSENT_ERROR_CODE, message=message)) return ToolExecutionException("MCP consent required", inner_exception=inner) class TestConsentUrlFromError: def test_returns_consent_url_when_inner_arg_is_consent_mcp_error(self) -> None: - exc = _make_consent_error("https://example.com/consent") - assert consent_url_from_error(exc) == "https://example.com/consent" + exc = _make_consent_error("https://example.com/consent", name="my-tool") + assert consent_url_from_error(exc) == [ConsentError(name="my-tool", consent_url="https://example.com/consent")] def test_returns_none_when_no_mcp_error_in_args(self) -> None: assert consent_url_from_error(Exception("boom")) is None @@ -3102,6 +3123,13 @@ class TestConsentUrlFromError: bare = McpError(ErrorData(code=CONSENT_ERROR_CODE, message="https://x")) assert consent_url_from_error(bare) is None + def test_returns_none_when_message_has_no_json(self) -> None: + from agent_framework.exceptions import ToolExecutionException + + inner = McpError(ErrorData(code=CONSENT_ERROR_CODE, message="no json here")) + exc = ToolExecutionException("MCP consent required", inner_exception=inner) + assert consent_url_from_error(exc) is None + class TestAgentLifecycle: async def test_agent_entered_lazily_on_first_request(self) -> None: 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 b9ced86300..7b7051dda4 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 @@ -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. 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. +> Definitions of these authentication methods can be found in the [agent.manifest.yaml](agent.manifest.yaml) file in this sample. The GitHub MCP connection defaults to using a 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).