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
Expand Up @@ -814,10 +814,16 @@ def get_mcp_tool(
if isinstance(approval_mode, str):
mcp["require_approval"] = "always" if approval_mode == "always_require" else "never"
else:
# Both keys can be supplied together ("and/or" per the docstring), so build
# one combined mapping instead of letting the second assignment overwrite
# the first, which silently dropped the always-approval tool list.
require_approval: dict[str, Any] = {}
if always_require := approval_mode.get("always_require_approval"):
mcp["require_approval"] = {"always": {"tool_names": always_require}}
require_approval["always"] = {"tool_names": always_require}
if never_require := approval_mode.get("never_require_approval"):
mcp["require_approval"] = {"never": {"tool_names": never_require}}
require_approval["never"] = {"tool_names": never_require}
if require_approval:
mcp["require_approval"] = require_approval

return mcp

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,23 @@ async def test_bad_request_error_non_content_filter() -> None:
assert "failed to complete the prompt" in str(exc_info.value)


def test_get_mcp_tool_merges_both_approval_lists() -> None:
"""Both approval lists must survive: overwriting the first with the second would
silently auto-approve tools the caller asked to gate."""
tool_config = FoundryChatClient.get_mcp_tool(
name="MCP",
url="https://mcp.example",
approval_mode={
"always_require_approval": ["dangerous_tool"],
"never_require_approval": ["safe_tool"],
},
)
assert tool_config["require_approval"] == {
"always": {"tool_names": ["dangerous_tool"]},
"never": {"tool_names": ["safe_tool"]},
}


def test_get_mcp_tool_with_project_connection_id() -> None:
tool_config = FoundryChatClient.get_mcp_tool(
name="Docs MCP",
Expand Down
15 changes: 13 additions & 2 deletions python/packages/openai/agent_framework_openai/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
CodeInterpreterContainerCodeInterpreterToolAuto,
ImageGeneration,
Mcp,
McpRequireApprovalMcpToolApprovalFilter,
McpRequireApprovalMcpToolApprovalFilterAlways,
McpRequireApprovalMcpToolApprovalFilterNever,
)
from openai.types.responses.web_search_tool_param import WebSearchToolParam
from pydantic import BaseModel, TypeAdapter, ValidationError
Expand Down Expand Up @@ -1641,10 +1644,18 @@ def get_mcp_tool(
if isinstance(approval_mode, str):
mcp["require_approval"] = "always" if approval_mode == "always_require" else "never"
else:
# Both keys can be supplied together ("and/or" per the docstring), so build
# one combined mapping instead of letting the second assignment overwrite
# the first, which silently dropped the always-approval tool list.
require_approval = McpRequireApprovalMcpToolApprovalFilter()
if always_require := approval_mode.get("always_require_approval"):
mcp["require_approval"] = {"always": {"tool_names": always_require}}
require_approval["always"] = McpRequireApprovalMcpToolApprovalFilterAlways(
tool_names=always_require
)
if never_require := approval_mode.get("never_require_approval"):
mcp["require_approval"] = {"never": {"tool_names": never_require}}
require_approval["never"] = McpRequireApprovalMcpToolApprovalFilterNever(tool_names=never_require)
if require_approval:
mcp["require_approval"] = require_approval

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.

Manohar Paturi (@ManoharPaturi) The combined accumulator is typed as dict[str, Any], but mcp["require_approval"] requires the SDK’s McpRequireApprovalMcpToolApprovalFilter mapping. This assignment fails Pyright and Pyrefly on the current head. Please initialize the accumulator with McpRequireApprovalMcpToolApprovalFilter() instead of a generic dictionary, preserving the combined-list logic and empty-mapping guard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in c744def. The accumulator is now McpRequireApprovalMcpToolApprovalFilter with its Always and Never filter objects constructed explicitly, so the require_approval assignment type checks against the Mcp tool parameter; the combined list logic and the empty mapping guard are unchanged, as is the emitted JSON. Verified with pyrefly directly: the file reports 1 error on the previous head and 0 errors now, the mcp and full chat client suites still pass (583 tests across openai and foundry files), and ruff check and format are clean. The foundry helper builds an azure MCPTool rather than the openai parameter, so its accumulator stays a plain dict and never trips this check.


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.

Please apply this same combined-mapping fix and regression coverage to FoundryChatClient.get_mcp_tool in python/packages/foundry/agent_framework_foundry/_chat_client.py. That public sibling currently has the identical two-assignment overwrite, so supplying both lists still drops always_require_approval for Foundry users and silently weakens the requested approval policy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. FoundryChatClient.get_mcp_tool now builds the same combined require_approval mapping, with a matching regression test asserting both lists survive together. It fails on the previous head of this branch and passes now, the full foundry chat client suite passes (104 tests), and ruff is clean on both files. Amended into the branch with the title updated to name both helpers.

return mcp

Expand Down
17 changes: 17 additions & 0 deletions python/packages/openai/tests/openai/test_openai_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6501,6 +6501,23 @@ def test_prepare_tools_for_openai_with_mcp() -> None:
assert "require_approval" in mcp


def test_get_mcp_tool_merges_both_approval_lists() -> None:
"""Both approval lists must survive: overwriting the first with the second would
silently auto-approve tools the caller asked to gate."""
tool_config = OpenAIChatClient.get_mcp_tool(
name="MCP",
url="https://mcp.example",
approval_mode={
"always_require_approval": ["dangerous_tool"],
"never_require_approval": ["safe_tool"],
},
)
assert tool_config["require_approval"] == {
"always": {"tool_names": ["dangerous_tool"]},
"never": {"tool_names": ["safe_tool"]},
}


@pytest.mark.parametrize("allowed_tools", [None, [], ["read_only"]], ids=["omitted", "empty", "nonempty"])
async def test_get_mcp_tool_preserves_allowed_tools_in_request(allowed_tools: list[str] | None) -> None:
tool_config = OpenAIChatClient.get_mcp_tool(
Expand Down
Loading