@@ -607,6 +607,14 @@ def test_get_mcp_tool_with_project_connection_id() -> None:
607607 assert tool_config ["project_connection_id" ] == "conn-123"
608608 assert tool_config ["allowed_tools" ] == ["search_docs" ]
609609 assert tool_config ["server_label" ] == "Docs_MCP"
610+ # ``server_url`` should not be fabricated when only a project connection is supplied.
611+ assert "server_url" not in tool_config
612+
613+
614+ def test_get_mcp_tool_requires_url_or_project_connection_id () -> None :
615+ """Missing both ``url`` and ``project_connection_id`` is always invalid."""
616+ with pytest .raises (ValueError , match = "url.*project_connection_id" ):
617+ FoundryChatClient .get_mcp_tool (name = "x" )
610618
611619
612620def test_prepare_tools_for_openai_strips_extraneous_name_from_foundry_mcp_tool () -> None :
@@ -655,6 +663,103 @@ def test_prepare_tools_for_openai_strips_read_model_fields_from_toolbox_code_int
655663 assert "description" not in prepared
656664
657665
666+ def test_prepare_tools_for_openai_injects_default_container_for_code_interpreter_dict () -> None :
667+ """Toolbox-returned code_interpreter without a container must get a default injected.
668+
669+ The Azure SDK treats ``container`` as optional, but the Responses API rejects
670+ ``code_interpreter`` entries without one. The sanitizer backfills ``{"type": "auto"}``.
671+ """
672+ project_client = MagicMock ()
673+ project_client .get_openai_client .return_value = _make_mock_openai_client ()
674+ client = FoundryChatClient (project_client = project_client , model = "test-model" )
675+
676+ tool = {
677+ "type" : "code_interpreter" ,
678+ "name" : "code_interpreter_t6bbtm" ,
679+ }
680+
681+ response_tools = client ._prepare_tools_for_openai ([tool ])
682+
683+ assert len (response_tools ) == 1
684+ prepared = response_tools [0 ]
685+ assert prepared ["type" ] == "code_interpreter"
686+ assert prepared ["container" ] == {"type" : "auto" }
687+ assert "name" not in prepared
688+
689+
690+ def test_prepare_tools_for_openai_injects_default_container_for_code_interpreter_sdk_instance () -> None :
691+ """SDK ``CodeInterpreterTool`` instances without a container must also be backfilled.
692+
693+ Reproduces the toolbox creation path that calls
694+ ``CodeInterpreterTool(name="code_interpreter")`` without a container.
695+ """
696+ from azure .ai .projects .models import CodeInterpreterTool
697+
698+ project_client = MagicMock ()
699+ project_client .get_openai_client .return_value = _make_mock_openai_client ()
700+ client = FoundryChatClient (project_client = project_client , model = "test-model" )
701+
702+ response_tools = client ._prepare_tools_for_openai ([CodeInterpreterTool (name = "code_interpreter" )])
703+
704+ assert len (response_tools ) == 1
705+ prepared = response_tools [0 ]
706+ assert prepared ["type" ] == "code_interpreter"
707+ assert prepared ["container" ] == {"type" : "auto" }
708+ assert "name" not in prepared
709+
710+
711+ def test_prepare_tools_for_openai_preserves_existing_code_interpreter_container () -> None :
712+ """An already-populated container must not be overwritten by the sanitizer."""
713+ project_client = MagicMock ()
714+ project_client .get_openai_client .return_value = _make_mock_openai_client ()
715+ client = FoundryChatClient (project_client = project_client , model = "test-model" )
716+
717+ explicit_container = {"file_ids" : ["file_123" ], "type" : "auto" }
718+ tool = {"type" : "code_interpreter" , "container" : explicit_container }
719+
720+ response_tools = client ._prepare_tools_for_openai ([tool ])
721+
722+ assert response_tools [0 ]["container" ] == explicit_container
723+
724+
725+ def test_prepare_tools_for_openai_rejects_file_search_without_vector_store_ids () -> None :
726+ """``file_search`` without ``vector_store_ids`` is always invalid — surface a clear error."""
727+ project_client = MagicMock ()
728+ project_client .get_openai_client .return_value = _make_mock_openai_client ()
729+ client = FoundryChatClient (project_client = project_client , model = "test-model" )
730+
731+ with pytest .raises (ValueError , match = "vector_store_ids" ):
732+ client ._prepare_tools_for_openai ([{"type" : "file_search" , "name" : "fs" }])
733+
734+
735+ def test_prepare_tools_for_openai_rejects_mcp_without_server_destination () -> None :
736+ """``mcp`` with neither ``server_url`` nor ``project_connection_id`` is always invalid."""
737+ project_client = MagicMock ()
738+ project_client .get_openai_client .return_value = _make_mock_openai_client ()
739+ client = FoundryChatClient (project_client = project_client , model = "test-model" )
740+
741+ tool = FoundryMCPTool (server_label = "orphan" )
742+
743+ with pytest .raises (ValueError , match = "server_url.*project_connection_id" ):
744+ client ._prepare_tools_for_openai ([tool ])
745+
746+
747+ def test_prepare_tools_for_openai_accepts_mcp_with_only_project_connection_id () -> None :
748+ """MCP tools backed by a Foundry connection (no ``server_url``) must still pass validation."""
749+ project_client = MagicMock ()
750+ project_client .get_openai_client .return_value = _make_mock_openai_client ()
751+ client = FoundryChatClient (project_client = project_client , model = "test-model" )
752+
753+ tool = FoundryMCPTool (server_label = "githubmcp" )
754+ tool ["project_connection_id" ] = "githubmcp"
755+
756+ response_tools = client ._prepare_tools_for_openai ([tool ])
757+
758+ assert len (response_tools ) == 1
759+ assert response_tools [0 ]["project_connection_id" ] == "githubmcp"
760+ assert "server_url" not in response_tools [0 ]
761+
762+
658763def test_prepare_tools_for_openai_strips_name_from_non_function_hosted_tool_dicts () -> None :
659764 """All non-function hosted tool payloads should drop top-level read-model names."""
660765 project_client = MagicMock ()
0 commit comments