Skip to content
Merged
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
Comment thread
eavanvalkenburg marked this conversation as resolved.
Resolved

Comment thread
eavanvalkenburg marked this conversation as resolved.
Resolved
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