From 9b9be6526f375c7e6e237bd1d14a76b7d24a4863 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 4 Jul 2026 03:29:26 +0530 Subject: [PATCH 01/12] feat(core): add support for MCP 2026 stateless draft and auto-negotiation fallback (#704) --- packages/toolbox-adk/tests/integration/test_integration.py | 2 ++ packages/toolbox-langchain/tests/test_e2e.py | 2 ++ packages/toolbox-llamaindex/tests/test_e2e.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/packages/toolbox-adk/tests/integration/test_integration.py b/packages/toolbox-adk/tests/integration/test_integration.py index 461b6133d..739eb79a4 100644 --- a/packages/toolbox-adk/tests/integration/test_integration.py +++ b/packages/toolbox-adk/tests/integration/test_integration.py @@ -37,6 +37,8 @@ pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") +pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") + # Ensure TOOLBOX_VERSION is set for the fixture if "TOOLBOX_VERSION" not in os.environ: os.environ["TOOLBOX_VERSION"] = "0.0.1" # Use a valid version or mock diff --git a/packages/toolbox-langchain/tests/test_e2e.py b/packages/toolbox-langchain/tests/test_e2e.py index 2a4a20bef..7ebcf9698 100644 --- a/packages/toolbox-langchain/tests/test_e2e.py +++ b/packages/toolbox-langchain/tests/test_e2e.py @@ -46,6 +46,8 @@ pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") +pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") + @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") diff --git a/packages/toolbox-llamaindex/tests/test_e2e.py b/packages/toolbox-llamaindex/tests/test_e2e.py index 4b0b2197c..9f07a8e1f 100644 --- a/packages/toolbox-llamaindex/tests/test_e2e.py +++ b/packages/toolbox-llamaindex/tests/test_e2e.py @@ -46,6 +46,8 @@ pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") +pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") + @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") From 3226712ab9413e323ed41ec87e183cb68cebb039 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 4 Jul 2026 03:29:30 +0530 Subject: [PATCH 02/12] feat(core): add custom protocols array support for ToolboxClient --- packages/toolbox-core/README.md | 17 +++++ .../toolbox-core/src/toolbox_core/client.py | 64 +++++++++++++++++- .../mcp_transport/transport_base.py | 4 ++ .../mcp_transport/v20250618/mcp.py | 2 +- .../mcp_transport/v20251125/mcp.py | 2 +- .../mcp_transport/v20260618/mcp.py | 32 +++++++-- packages/toolbox-core/tests/conftest.py | 2 + packages/toolbox-core/tests/constants.py | 16 +++++ packages/toolbox-core/tests/test_client.py | 31 ++++++++- packages/toolbox-core/tests/test_e2e.py | 5 +- packages/toolbox-core/tests/test_e2e_mcp.py | 59 ++++++++++++++--- packages/toolbox-core/tests/test_fallback.py | 66 +++++++++++++++++++ packages/toolbox-core/tests/test_sync_e2e.py | 3 +- .../src/toolbox_langchain/async_client.py | 3 +- .../src/toolbox_langchain/client.py | 3 +- .../src/toolbox_llamaindex/async_client.py | 3 +- .../src/toolbox_llamaindex/client.py | 3 +- 17 files changed, 287 insertions(+), 28 deletions(-) create mode 100644 packages/toolbox-core/tests/constants.py create mode 100644 packages/toolbox-core/tests/test_fallback.py diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 60daf7580..4f57e4a01 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -58,12 +58,29 @@ The core package provides a framework-agnostic way to interact with your Toolbox - [Loading Tools](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#loading-tools) - [Invoking Tools](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#invoking-tools) - [Synchronous Usage](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#synchronous-usage) +- [Protocol Negotiation](#protocol-negotiation) - [Use With Langraph](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#use-with-langgraph) + - [Client to Server Authentication](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#client-to-server-authentication) - [Authenticating Tools](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#authenticating-tools) - [Binding Parameter Values](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#parameter-binding) - [OpenTelemetry](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#opentelemetry) +## Protocol Negotiation + +By default, the client negotiates the newest protocol version supported by the server. You can provide a custom list of supported protocols to restrict negotiation to specific versions or a single version. Ensure you pass the [`Protocol`](src/toolbox_core/protocol.py) enum constants. Both `Protocol.MCP_LATEST` and `Protocol.MCP_DRAFT` are supported as well. + +```py +from toolbox_core import ToolboxClient, Protocol + +async def main(): + async with ToolboxClient( + "http://127.0.0.1:5000", + protocol=[Protocol.MCP_LATEST, Protocol.MCP_DRAFT] + ) as toolbox: + pass +``` + # Contributing diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index b2224eedc..289173175 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -53,12 +53,14 @@ def __init__( client_name: Optional[str], client_version: Optional[str], telemetry_enabled: bool, + supported_protocols: Optional[list[str]] = None, ): self._url = url self._session = session self._client_name = client_name self._client_version = client_version self._telemetry_enabled = telemetry_enabled + self._supported_protocols = supported_protocols self._active_transport = self._create_transport(protocol) def _create_transport(self, protocol: Protocol) -> ITransport: @@ -71,6 +73,7 @@ def _create_transport(self, protocol: Protocol) -> ITransport: self._client_name, self._client_version, telemetry_enabled=self._telemetry_enabled, + supported_protocols=self._supported_protocols, ) case Protocol.MCP_v20251125: return McpHttpTransportV20251125( @@ -80,6 +83,7 @@ def _create_transport(self, protocol: Protocol) -> ITransport: self._client_name, self._client_version, telemetry_enabled=self._telemetry_enabled, + supported_protocols=self._supported_protocols, ) case Protocol.MCP_v20250618: return McpHttpTransportV20250618( @@ -89,6 +93,7 @@ def _create_transport(self, protocol: Protocol) -> ITransport: self._client_name, self._client_version, telemetry_enabled=self._telemetry_enabled, + supported_protocols=self._supported_protocols, ) case Protocol.MCP_v20250326: return McpHttpTransportV20250326( @@ -98,6 +103,7 @@ def _create_transport(self, protocol: Protocol) -> ITransport: self._client_name, self._client_version, telemetry_enabled=self._telemetry_enabled, + supported_protocols=self._supported_protocols, ) case Protocol.MCP_v20241105: return McpHttpTransportV20241105( @@ -107,6 +113,7 @@ def _create_transport(self, protocol: Protocol) -> ITransport: self._client_name, self._client_version, telemetry_enabled=self._telemetry_enabled, + supported_protocols=self._supported_protocols, ) case _: raise ValueError(f"Unsupported MCP protocol version: {protocol}") @@ -126,7 +133,33 @@ async def _execute_with_fallback( try: return await getattr(self._active_transport, method_name)(*args, **kwargs) except ProtocolNegotiationError as e: - fallback_protocol = Protocol(e.negotiated_version) + server_version = e.negotiated_version + all_versions = Protocol.get_supported_mcp_versions() + + try: + server_idx = all_versions.index(server_version) + except ValueError: + raise RuntimeError( + f"Server returned unknown protocol version: {server_version}" + ) + + # Artificial Array: Server supports this version and all older ones + server_supported = all_versions[server_idx:] + + if self._supported_protocols: + client_supported = self._supported_protocols + mutually_supported = [v for v in client_supported if v in server_supported] + if mutually_supported: + fallback_protocol = Protocol(mutually_supported[0]) + else: + raise RuntimeError( + "No mutually supported protocol version. " + f"Client supports: {client_supported}, " + f"Server supports (and older): {server_version}" + ) + else: + fallback_protocol = Protocol(server_version) + logging.warning( f"Protocol fallback required. Switching from " f"{self._protocol_version} to {fallback_protocol.value}" @@ -166,7 +199,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol = Protocol.MCP, + protocol: Union[Protocol, list[Protocol], list[str]] = Protocol.MCP, client_name: Optional[str] = None, client_version: Optional[str] = None, telemetry_enabled: bool = False, @@ -188,13 +221,38 @@ def __init__( telemetry_enabled: Whether to enable OpenTelemetry tracing and metrics. (Default: False) """ + if isinstance(protocol, list): + if not protocol: + raise ValueError("protocol list cannot be empty") + user_protocols = [ + p.value if isinstance(p, Protocol) else str(p) for p in protocol + ] + + supported_mcp_versions = Protocol.get_supported_mcp_versions() + for p in user_protocols: + if p not in supported_mcp_versions: + raise ValueError( + f"Invalid protocol version '{p}'. Must be one of: {supported_mcp_versions}" + ) + + user_protocols_set = set(user_protocols) + # Intersect with the globally sorted list to strictly enforce newest-to-oldest ordering + supported_protocols = [ + v for v in supported_mcp_versions if v in user_protocols_set + ] + initial_protocol = Protocol(supported_protocols[0]) + else: + supported_protocols = None + initial_protocol = protocol + self.__transport = _McpTransportProxy( url, session, - protocol, + initial_protocol, client_name, client_version, telemetry_enabled, + supported_protocols, ) self.__client_headers = client_headers if client_headers is not None else {} diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/transport_base.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/transport_base.py index 735ae9066..9ffc2ebc8 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/transport_base.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/transport_base.py @@ -42,6 +42,7 @@ def __init__( client_name: Optional[str] = None, client_version: Optional[str] = None, telemetry_enabled: bool = False, + supported_protocols: Optional[list[str]] = None, ): self._mcp_base_url = f"{base_url}/mcp/" self._protocol_version = protocol.value @@ -50,6 +51,9 @@ def __init__( self._client_name = client_name self._client_version = client_version self._telemetry_enabled = telemetry.resolve_telemetry_enabled(telemetry_enabled) + self._supported_protocols = ( + supported_protocols or Protocol.get_supported_mcp_versions() + ) self._tracer: Optional[telemetry.Tracer] = None self._operation_duration_histogram: Optional[telemetry.Histogram] = None diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py index 2ea3ee636..35f759346 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py @@ -75,7 +75,7 @@ async def _send_request( err_val = json_resp["error"] if isinstance(err_val, dict) and err_val.get("code") == -32004: server_supported = err_val.get("data", {}).get("supported", []) - client_supported = Protocol.get_supported_mcp_versions() + client_supported = self._supported_protocols mutually_supported = [ v for v in client_supported if v in server_supported ] diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/mcp.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/mcp.py index 4a0e8d876..bf365dece 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/mcp.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/mcp.py @@ -75,7 +75,7 @@ async def _send_request( err_val = json_resp["error"] if isinstance(err_val, dict) and err_val.get("code") == -32004: server_supported = err_val.get("data", {}).get("supported", []) - client_supported = Protocol.get_supported_mcp_versions() + client_supported = self._supported_protocols mutually_supported = [ v for v in client_supported if v in server_supported ] diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260618/mcp.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260618/mcp.py index 866249733..2687741c4 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260618/mcp.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260618/mcp.py @@ -88,7 +88,10 @@ async def _send_request( "supported", [] ) - client_supported = Protocol.get_supported_mcp_versions() + client_supported = ( + self._supported_protocols + or Protocol.get_supported_mcp_versions() + ) mutually_supported = [ v for v in client_supported if v in server_supported ] @@ -105,10 +108,24 @@ async def _send_request( isinstance(err_val, str) and "invalid protocol version" in err_val.lower() ): - # Legacy 2025-06-18 servers don't use the -32004 code or provide - # a supported versions list. They return this raw string error - # instead. We safely assume 2025-06-18 here. - raise ProtocolNegotiationError(Protocol.MCP_v20250618) + # Cascading Fallback: Legacy servers throw this string error. + # We pick the next version from the user's supported list. + client_supported = ( + self._supported_protocols + or Protocol.get_supported_mcp_versions() + ) + try: + current_idx = client_supported.index(self._protocol_version) + if current_idx + 1 < len(client_supported): + raise ProtocolNegotiationError(client_supported[current_idx + 1]) + else: + raise RuntimeError( + "Server threw 'invalid protocol version' but no fallback versions " + "remain in the user's supported protocols array." + ) + except ValueError: + # Current version not in list somehow, just fallback to highest stateful + raise ProtocolNegotiationError(Protocol.MCP_v20251125) except Exception as e: if isinstance(e, (RuntimeError, ProtocolNegotiationError)): raise e @@ -131,7 +148,10 @@ async def _send_request( err_val = json_resp["error"] if isinstance(err_val, dict) and err_val.get("code") == -32004: server_supported = err_val.get("data", {}).get("supported", []) - client_supported = Protocol.get_supported_mcp_versions() + client_supported = ( + self._supported_protocols + or Protocol.get_supported_mcp_versions() + ) mutually_supported = [ v for v in client_supported if v in server_supported ] diff --git a/packages/toolbox-core/tests/conftest.py b/packages/toolbox-core/tests/conftest.py index b78db9db6..cf8aba758 100644 --- a/packages/toolbox-core/tests/conftest.py +++ b/packages/toolbox-core/tests/conftest.py @@ -32,6 +32,8 @@ TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" +from tests.constants import TOOLBOX_SERVER_URL_DRAFT, TOOLBOX_SERVER_URL_STABLE + #### Define Utility Functions def get_env_var(key: str) -> str: diff --git a/packages/toolbox-core/tests/constants.py b/packages/toolbox-core/tests/constants.py new file mode 100644 index 000000000..701f830e6 --- /dev/null +++ b/packages/toolbox-core/tests/constants.py @@ -0,0 +1,16 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" +TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index e28bb03a1..f2cdf7157 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -21,6 +21,7 @@ import pytest from aiohttp import web +from tests.constants import TOOLBOX_SERVER_URL_STABLE from toolbox_core.client import ToolboxClient from toolbox_core.itransport import ITransport from toolbox_core.protocol import ( @@ -814,7 +815,7 @@ def test_toolbox_client_no_warning_on_mcp(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - client = ToolboxClient("http://localhost:5000", protocol=Protocol.MCP) + client = ToolboxClient(TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP) assert len(w) == 0 @@ -825,6 +826,32 @@ def test_toolbox_client_no_warning_on_explicit_mcp_version(): warnings.simplefilter("always") client = ToolboxClient( - "http://localhost:5000", protocol=Protocol.MCP_v20251125 + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 ) assert len(w) == 0 + + +def test_toolbox_client_custom_protocols(): + """Test that custom protocols array is correctly parsed and sorted.""" + with patch("toolbox_core.client._McpTransportProxy") as mock_proxy: + client = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, + protocol=[Protocol.MCP_v20241105, Protocol.MCP_DRAFT, "2025-06-18"], + ) + mock_proxy.assert_called_once() + args, kwargs = mock_proxy.call_args + + # Check initial_protocol + assert args[2] == Protocol.MCP_DRAFT + # Check supported_protocols (must be sorted from newest to oldest) + assert args[6] == ["DRAFT-2026-v1", "2025-06-18", "2024-11-05"] + + +def test_toolbox_client_custom_protocols_invalid(): + """Test that custom protocols array raises error on invalid inputs.""" + + with pytest.raises(ValueError, match="protocol list cannot be empty"): + ToolboxClient(TOOLBOX_SERVER_URL_STABLE, protocol=[]) + + with pytest.raises(ValueError, match="Invalid protocol version 'invalid-version'"): + ToolboxClient(TOOLBOX_SERVER_URL_STABLE, protocol=["invalid-version"]) diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 9f1f6b01f..920360812 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -19,6 +19,7 @@ import pytest_asyncio from pydantic import ValidationError +from tests.constants import TOOLBOX_SERVER_URL_STABLE from toolbox_core.client import ToolboxClient from toolbox_core.protocol import Protocol from toolbox_core.tool import ToolboxTool @@ -30,7 +31,7 @@ @pytest_asyncio.fixture(scope="function") async def toolbox(): """Creates a ToolboxClient instance shared by all tests in this module.""" - toolbox = ToolboxClient("http://localhost:5000", protocol=Protocol.MCP) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP) try: yield toolbox finally: @@ -114,7 +115,7 @@ async def test_run_tool_wrong_param_type(self, get_n_rows_tool: ToolboxTool): async def test_load_and_run_tool_with_telemetry(self, telemetry_enabled: bool): """Load and invoke a tool with telemetry_enabled=True/False.""" async with ToolboxClient( - "http://localhost:5000", + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP, telemetry_enabled=telemetry_enabled, ) as toolbox: diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index 890705da4..a3a838320 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -19,6 +19,7 @@ import pytest_asyncio from pydantic import ValidationError +from tests.constants import TOOLBOX_SERVER_URL_DRAFT, TOOLBOX_SERVER_URL_STABLE from toolbox_core.client import ToolboxClient from toolbox_core.protocol import Protocol from toolbox_core.tool import ToolboxTool @@ -32,7 +33,7 @@ ) async def toolbox(request): """Creates a ToolboxClient instance shared by all tests in this module.""" - toolbox = ToolboxClient("http://localhost:5000", protocol=Protocol(request.param)) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL_STABLE, protocol=Protocol(request.param)) try: yield toolbox finally: @@ -102,8 +103,8 @@ async def test_run_tool_missing_params(self, get_n_rows_tool: ToolboxTool): async def test_protocol_fallback_e2e(self, toolbox_server_url: str): """Tests that a client using MCP_DRAFT can fallback to an older protocol against a server that doesn't support the draft version.""" - # The E2E server currently does not support DRAFT 2026 on port 5000, so this will trigger a fallback. - # However, port 5001 does support DRAFT 2026. + # The stable server currently does not support DRAFT 2026, so this will trigger a fallback. + # However, the draft server does support DRAFT 2026. async with ToolboxClient( toolbox_server_url, protocol=Protocol.MCP_DRAFT ) as client: @@ -111,7 +112,7 @@ async def test_protocol_fallback_e2e(self, toolbox_server_url: str): response = await tool(num_rows="1") assert "row1" in response # Verify that fallback occurred by checking the transport's final protocol version - if "5001" in toolbox_server_url: + if toolbox_server_url == TOOLBOX_SERVER_URL_DRAFT: assert ( client._ToolboxClient__transport._protocol_version == Protocol.MCP_DRAFT.value @@ -474,9 +475,9 @@ async def test_run_tool_with_wrong_map_value_type(self, toolbox: ToolboxClient): @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") -async def test_mcp_default_protocol(): +async def test_mcp_default_protocol(toolbox_server_url: str): """Verify that omitting the protocol argument defaults correctly and works.""" - async with ToolboxClient("http://localhost:5000") as client: + async with ToolboxClient(toolbox_server_url) as client: tool = await client.load_tool("get-n-rows") response = await tool(num_rows="1") assert "row1" in response @@ -484,11 +485,53 @@ async def test_mcp_default_protocol(): @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") -async def test_mcp_draft_fallback(): +async def test_mcp_draft_fallback(toolbox_server_url: str): """Verify that explicitly using MCP_DRAFT against a server that doesn't support it falls back successfully.""" + async with ToolboxClient(toolbox_server_url, protocol=Protocol.MCP_DRAFT) as client: + tool = await client.load_tool("get-n-rows") + response = await tool(num_rows="1") + assert "row1" in response + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("toolbox_server") +async def test_mcp_latest_protocol(toolbox_server_url: str): + """Verify that explicitly using MCP_LATEST works successfully.""" + async with ToolboxClient( + toolbox_server_url, protocol=Protocol.MCP_LATEST + ) as client: + tool = await client.load_tool("get-n-rows") + response = await tool(num_rows="1") + assert "row1" in response + assert ( + client._ToolboxClient__transport._protocol_version + == Protocol.MCP_LATEST.value + ) + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("toolbox_server") +async def test_mcp_custom_protocols_list(toolbox_server_url: str): + """Verify that passing a list of protocols with MCP_LATEST and MCP_DRAFT works successfully.""" async with ToolboxClient( - "http://localhost:5000", protocol=Protocol.MCP_DRAFT + toolbox_server_url, + protocol=[ + Protocol.MCP_v20241105, + Protocol.MCP_v20250326, + Protocol.MCP_LATEST, + Protocol.MCP_DRAFT + ] ) as client: tool = await client.load_tool("get-n-rows") response = await tool(num_rows="1") assert "row1" in response + if toolbox_server_url == TOOLBOX_SERVER_URL_STABLE: + assert ( + client._ToolboxClient__transport._protocol_version + == Protocol.MCP_LATEST.value + ) + else: + assert ( + client._ToolboxClient__transport._protocol_version + == Protocol.MCP_DRAFT.value + ) diff --git a/packages/toolbox-core/tests/test_fallback.py b/packages/toolbox-core/tests/test_fallback.py new file mode 100644 index 000000000..1616b9c28 --- /dev/null +++ b/packages/toolbox-core/tests/test_fallback.py @@ -0,0 +1,66 @@ +import pytest +from unittest.mock import AsyncMock, patch +from toolbox_core.client import _McpTransportProxy +from toolbox_core.protocol import Protocol +from toolbox_core.exceptions import ProtocolNegotiationError + +@pytest.mark.asyncio +async def test_artificial_array(): + """The Artificial Array Test: simulate server returning 2025-03-26, should fallback to 2024-11-05.""" + proxy = _McpTransportProxy("http://mock", None, Protocol.MCP_DRAFT, None, None, False, [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value]) + + proxy._active_transport.tool_get = AsyncMock(side_effect=ProtocolNegotiationError(Protocol.MCP_v20250326.value)) + + with patch.object(proxy, "_create_transport") as mock_create: + mock_new_transport = AsyncMock() + mock_new_transport.tool_get.return_value = "success" + mock_create.return_value = mock_new_transport + + res = await proxy.tool_get("mock") + + assert res == "success" + mock_create.assert_called_with(Protocol.MCP_v20241105) + +@pytest.mark.asyncio +async def test_cascading_fallback(): + """The Cascading Fallback Test: simulate server stateless generic error which throws the next stateful version.""" + proxy = _McpTransportProxy("http://mock", None, Protocol.MCP_DRAFT, None, None, False, [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value]) + + proxy._active_transport.tool_get = AsyncMock(side_effect=ProtocolNegotiationError(Protocol.MCP_v20251125.value)) + + with patch.object(proxy, "_create_transport") as mock_create: + mock_new_transport = AsyncMock() + mock_new_transport.tool_get.return_value = "success" + mock_create.return_value = mock_new_transport + + res = await proxy.tool_get("mock") + + assert res == "success" + mock_create.assert_called_with(Protocol.MCP_v20251125) + +@pytest.mark.asyncio +async def test_strict_constraint(): + """The Strict Constraint Test: simulate legacy server returning an unsupported old version.""" + proxy = _McpTransportProxy("http://mock", None, Protocol.MCP_DRAFT, None, None, False, [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value]) + + proxy._active_transport.tool_get = AsyncMock(side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value)) + + with pytest.raises(RuntimeError, match="No mutually supported protocol version"): + await proxy.tool_get("mock") + +@pytest.mark.asyncio +async def test_modern_smart_fallback(): + """The Modern Smart-Fallback Test: simulate modern payload correctly returning pre-intersected fallback.""" + proxy = _McpTransportProxy("http://mock", None, Protocol.MCP_DRAFT, None, None, False, [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value]) + + proxy._active_transport.tool_get = AsyncMock(side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value)) + + with patch.object(proxy, "_create_transport") as mock_create: + mock_new_transport = AsyncMock() + mock_new_transport.tool_get.return_value = "success" + mock_create.return_value = mock_new_transport + + res = await proxy.tool_get("mock") + + assert res == "success" + mock_create.assert_called_with(Protocol.MCP_v20241105) diff --git a/packages/toolbox-core/tests/test_sync_e2e.py b/packages/toolbox-core/tests/test_sync_e2e.py index 38b90b131..172db9107 100644 --- a/packages/toolbox-core/tests/test_sync_e2e.py +++ b/packages/toolbox-core/tests/test_sync_e2e.py @@ -14,6 +14,7 @@ import pytest +from tests.constants import TOOLBOX_SERVER_URL_STABLE from toolbox_core.sync_client import ToolboxSyncClient from toolbox_core.sync_tool import ToolboxSyncTool @@ -24,7 +25,7 @@ @pytest.fixture(scope="module") def toolbox(): """Creates a ToolboxSyncClient instance shared by all tests in this module.""" - toolbox = ToolboxSyncClient("http://localhost:5000") + toolbox = ToolboxSyncClient(TOOLBOX_SERVER_URL_STABLE) try: yield toolbox finally: diff --git a/packages/toolbox-langchain/src/toolbox_langchain/async_client.py b/packages/toolbox-langchain/src/toolbox_langchain/async_client.py index 8d389d8c2..5f516c22a 100644 --- a/packages/toolbox-langchain/src/toolbox_langchain/async_client.py +++ b/packages/toolbox-langchain/src/toolbox_langchain/async_client.py @@ -17,6 +17,7 @@ from aiohttp import ClientSession from toolbox_core.client import ToolboxClient as ToolboxCoreClient +from typing import Any, Sequence from toolbox_core.protocol import Protocol from .async_tools import AsyncToolboxTool @@ -35,7 +36,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol = Protocol.MCP, + protocol: Protocol | Sequence[Protocol] = Protocol.MCP, telemetry_enabled: bool = False, ): """ diff --git a/packages/toolbox-langchain/src/toolbox_langchain/client.py b/packages/toolbox-langchain/src/toolbox_langchain/client.py index e0f607303..633cc70a4 100644 --- a/packages/toolbox-langchain/src/toolbox_langchain/client.py +++ b/packages/toolbox-langchain/src/toolbox_langchain/client.py @@ -16,6 +16,7 @@ from typing import Any, Awaitable, Callable, Mapping, Optional, Union from warnings import warn +from typing import Any, Sequence from toolbox_core.protocol import Protocol from toolbox_core.sync_client import ToolboxSyncClient as ToolboxCoreSyncClient @@ -31,7 +32,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol = Protocol.MCP, + protocol: Protocol | Sequence[Protocol] = Protocol.MCP, telemetry_enabled: bool = False, ) -> None: """ diff --git a/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py b/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py index e38563ee0..150a2ff2a 100644 --- a/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py +++ b/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py @@ -17,6 +17,7 @@ from aiohttp import ClientSession from toolbox_core.client import ToolboxClient as ToolboxCoreClient +from typing import Any, Sequence from toolbox_core.protocol import Protocol from .async_tools import AsyncToolboxTool @@ -35,7 +36,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol = Protocol.MCP, + protocol: Protocol | Sequence[Protocol] = Protocol.MCP, telemetry_enabled: bool = False, ): """ diff --git a/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py b/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py index 8cd7d16ce..6f3e63b0d 100644 --- a/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py +++ b/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py @@ -16,6 +16,7 @@ from typing import Any, Awaitable, Callable, Mapping, Optional, Union from warnings import warn +from typing import Any, Sequence from toolbox_core.protocol import Protocol from toolbox_core.sync_client import ToolboxSyncClient as ToolboxCoreSyncClient from toolbox_core.sync_tool import ToolboxSyncTool @@ -32,7 +33,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol = Protocol.MCP, + protocol: Protocol | Sequence[Protocol] = Protocol.MCP, telemetry_enabled: bool = False, ) -> None: """ From 85dd4ad727c76bd192f2a21a4a5854aacabf15b4 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 4 Jul 2026 04:21:49 +0530 Subject: [PATCH 03/12] resolve PR 707 review comments --- packages/toolbox-core/README.md | 3 +-- packages/toolbox-core/src/toolbox_core/client.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 4f57e4a01..77c008d74 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -58,13 +58,12 @@ The core package provides a framework-agnostic way to interact with your Toolbox - [Loading Tools](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#loading-tools) - [Invoking Tools](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#invoking-tools) - [Synchronous Usage](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#synchronous-usage) -- [Protocol Negotiation](#protocol-negotiation) - [Use With Langraph](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#use-with-langgraph) - - [Client to Server Authentication](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#client-to-server-authentication) - [Authenticating Tools](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#authenticating-tools) - [Binding Parameter Values](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#parameter-binding) - [OpenTelemetry](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#opentelemetry) +- [Protocol Negotiation](#protocol-negotiation) ## Protocol Negotiation diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index 289173175..d49859f40 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -215,7 +215,9 @@ def __init__( should typically be managed externally. client_headers: Headers to include in each request sent through this client. - protocol: The communication protocol to use. + protocol: The communication protocol to use. Can be a single version or a list of versions. + If a single version is provided (e.g. Protocol.MCP_LATEST), the client falls back through all older versions during negotiation. + If a list of versions is provided (e.g. [Protocol.MCP_LATEST, "2025-11-25"]), negotiation is restricted to those versions, and the newest version in the list is negotiated for first. client_name: Optional client name for identification. client_version: Optional client version for identification. telemetry_enabled: Whether to enable OpenTelemetry tracing and metrics. (Default: False) From cfb15966f5a75a0a45e819ee0df96172e2cbedd6 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 4 Jul 2026 04:29:18 +0530 Subject: [PATCH 04/12] remove redundant constant definitions in conftest.py --- .../toolbox-core/src/toolbox_core/client.py | 4 +- .../mcp_transport/v20260618/mcp.py | 8 +- packages/toolbox-core/tests/conftest.py | 4 +- packages/toolbox-core/tests/test_e2e_mcp.py | 12 +-- packages/toolbox-core/tests/test_fallback.py | 94 ++++++++++++++----- .../src/toolbox_langchain/async_client.py | 3 +- .../src/toolbox_langchain/client.py | 3 +- .../src/toolbox_llamaindex/async_client.py | 3 +- .../src/toolbox_llamaindex/client.py | 3 +- 9 files changed, 91 insertions(+), 43 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index d49859f40..ab427b640 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -148,7 +148,9 @@ async def _execute_with_fallback( if self._supported_protocols: client_supported = self._supported_protocols - mutually_supported = [v for v in client_supported if v in server_supported] + mutually_supported = [ + v for v in client_supported if v in server_supported + ] if mutually_supported: fallback_protocol = Protocol(mutually_supported[0]) else: diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260618/mcp.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260618/mcp.py index 2687741c4..2955b72e7 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260618/mcp.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260618/mcp.py @@ -115,9 +115,13 @@ async def _send_request( or Protocol.get_supported_mcp_versions() ) try: - current_idx = client_supported.index(self._protocol_version) + current_idx = client_supported.index( + self._protocol_version + ) if current_idx + 1 < len(client_supported): - raise ProtocolNegotiationError(client_supported[current_idx + 1]) + raise ProtocolNegotiationError( + client_supported[current_idx + 1] + ) else: raise RuntimeError( "Server threw 'invalid protocol version' but no fallback versions " diff --git a/packages/toolbox-core/tests/conftest.py b/packages/toolbox-core/tests/conftest.py index cf8aba758..b174dc258 100644 --- a/packages/toolbox-core/tests/conftest.py +++ b/packages/toolbox-core/tests/conftest.py @@ -29,11 +29,11 @@ from google.auth import compute_engine from google.cloud import secretmanager, storage +from tests.constants import TOOLBOX_SERVER_URL_DRAFT, TOOLBOX_SERVER_URL_STABLE + TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" -from tests.constants import TOOLBOX_SERVER_URL_DRAFT, TOOLBOX_SERVER_URL_STABLE - #### Define Utility Functions def get_env_var(key: str) -> str: diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index a3a838320..8e1b7c99d 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -514,13 +514,13 @@ async def test_mcp_latest_protocol(toolbox_server_url: str): async def test_mcp_custom_protocols_list(toolbox_server_url: str): """Verify that passing a list of protocols with MCP_LATEST and MCP_DRAFT works successfully.""" async with ToolboxClient( - toolbox_server_url, + toolbox_server_url, protocol=[ - Protocol.MCP_v20241105, - Protocol.MCP_v20250326, - Protocol.MCP_LATEST, - Protocol.MCP_DRAFT - ] + Protocol.MCP_v20241105, + Protocol.MCP_v20250326, + Protocol.MCP_LATEST, + Protocol.MCP_DRAFT, + ], ) as client: tool = await client.load_tool("get-n-rows") response = await tool(num_rows="1") diff --git a/packages/toolbox-core/tests/test_fallback.py b/packages/toolbox-core/tests/test_fallback.py index 1616b9c28..f3be223fd 100644 --- a/packages/toolbox-core/tests/test_fallback.py +++ b/packages/toolbox-core/tests/test_fallback.py @@ -1,66 +1,112 @@ -import pytest from unittest.mock import AsyncMock, patch + +import pytest + from toolbox_core.client import _McpTransportProxy -from toolbox_core.protocol import Protocol from toolbox_core.exceptions import ProtocolNegotiationError +from toolbox_core.protocol import Protocol + @pytest.mark.asyncio async def test_artificial_array(): """The Artificial Array Test: simulate server returning 2025-03-26, should fallback to 2024-11-05.""" - proxy = _McpTransportProxy("http://mock", None, Protocol.MCP_DRAFT, None, None, False, [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value]) - - proxy._active_transport.tool_get = AsyncMock(side_effect=ProtocolNegotiationError(Protocol.MCP_v20250326.value)) - + proxy = _McpTransportProxy( + "http://mock", + None, + Protocol.MCP_DRAFT, + None, + None, + False, + [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value], + ) + + proxy._active_transport.tool_get = AsyncMock( + side_effect=ProtocolNegotiationError(Protocol.MCP_v20250326.value) + ) + with patch.object(proxy, "_create_transport") as mock_create: mock_new_transport = AsyncMock() mock_new_transport.tool_get.return_value = "success" mock_create.return_value = mock_new_transport - + res = await proxy.tool_get("mock") - + assert res == "success" mock_create.assert_called_with(Protocol.MCP_v20241105) + @pytest.mark.asyncio async def test_cascading_fallback(): """The Cascading Fallback Test: simulate server stateless generic error which throws the next stateful version.""" - proxy = _McpTransportProxy("http://mock", None, Protocol.MCP_DRAFT, None, None, False, [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value]) - - proxy._active_transport.tool_get = AsyncMock(side_effect=ProtocolNegotiationError(Protocol.MCP_v20251125.value)) - + proxy = _McpTransportProxy( + "http://mock", + None, + Protocol.MCP_DRAFT, + None, + None, + False, + [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value], + ) + + proxy._active_transport.tool_get = AsyncMock( + side_effect=ProtocolNegotiationError(Protocol.MCP_v20251125.value) + ) + with patch.object(proxy, "_create_transport") as mock_create: mock_new_transport = AsyncMock() mock_new_transport.tool_get.return_value = "success" mock_create.return_value = mock_new_transport - + res = await proxy.tool_get("mock") - + assert res == "success" mock_create.assert_called_with(Protocol.MCP_v20251125) + @pytest.mark.asyncio async def test_strict_constraint(): """The Strict Constraint Test: simulate legacy server returning an unsupported old version.""" - proxy = _McpTransportProxy("http://mock", None, Protocol.MCP_DRAFT, None, None, False, [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value]) - - proxy._active_transport.tool_get = AsyncMock(side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value)) - + proxy = _McpTransportProxy( + "http://mock", + None, + Protocol.MCP_DRAFT, + None, + None, + False, + [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value], + ) + + proxy._active_transport.tool_get = AsyncMock( + side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value) + ) + with pytest.raises(RuntimeError, match="No mutually supported protocol version"): await proxy.tool_get("mock") + @pytest.mark.asyncio async def test_modern_smart_fallback(): """The Modern Smart-Fallback Test: simulate modern payload correctly returning pre-intersected fallback.""" - proxy = _McpTransportProxy("http://mock", None, Protocol.MCP_DRAFT, None, None, False, [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value]) - - proxy._active_transport.tool_get = AsyncMock(side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value)) - + proxy = _McpTransportProxy( + "http://mock", + None, + Protocol.MCP_DRAFT, + None, + None, + False, + [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value], + ) + + proxy._active_transport.tool_get = AsyncMock( + side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value) + ) + with patch.object(proxy, "_create_transport") as mock_create: mock_new_transport = AsyncMock() mock_new_transport.tool_get.return_value = "success" mock_create.return_value = mock_new_transport - + res = await proxy.tool_get("mock") - + assert res == "success" mock_create.assert_called_with(Protocol.MCP_v20241105) diff --git a/packages/toolbox-langchain/src/toolbox_langchain/async_client.py b/packages/toolbox-langchain/src/toolbox_langchain/async_client.py index 5f516c22a..d5035b966 100644 --- a/packages/toolbox-langchain/src/toolbox_langchain/async_client.py +++ b/packages/toolbox-langchain/src/toolbox_langchain/async_client.py @@ -12,12 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Awaitable, Callable, Mapping, Optional, Union +from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Union from warnings import warn from aiohttp import ClientSession from toolbox_core.client import ToolboxClient as ToolboxCoreClient -from typing import Any, Sequence from toolbox_core.protocol import Protocol from .async_tools import AsyncToolboxTool diff --git a/packages/toolbox-langchain/src/toolbox_langchain/client.py b/packages/toolbox-langchain/src/toolbox_langchain/client.py index 633cc70a4..677a0df02 100644 --- a/packages/toolbox-langchain/src/toolbox_langchain/client.py +++ b/packages/toolbox-langchain/src/toolbox_langchain/client.py @@ -13,10 +13,9 @@ # limitations under the License. from asyncio import to_thread -from typing import Any, Awaitable, Callable, Mapping, Optional, Union +from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Union from warnings import warn -from typing import Any, Sequence from toolbox_core.protocol import Protocol from toolbox_core.sync_client import ToolboxSyncClient as ToolboxCoreSyncClient diff --git a/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py b/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py index 150a2ff2a..58bb5b142 100644 --- a/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py +++ b/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py @@ -12,12 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Awaitable, Callable, Mapping, Optional, Union +from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Union from warnings import warn from aiohttp import ClientSession from toolbox_core.client import ToolboxClient as ToolboxCoreClient -from typing import Any, Sequence from toolbox_core.protocol import Protocol from .async_tools import AsyncToolboxTool diff --git a/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py b/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py index 6f3e63b0d..e653c2984 100644 --- a/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py +++ b/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py @@ -13,10 +13,9 @@ # limitations under the License. from asyncio import to_thread -from typing import Any, Awaitable, Callable, Mapping, Optional, Union +from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Union from warnings import warn -from typing import Any, Sequence from toolbox_core.protocol import Protocol from toolbox_core.sync_client import ToolboxSyncClient as ToolboxCoreSyncClient from toolbox_core.sync_tool import ToolboxSyncTool From 19bdf39b4ac0e05cac3a99c6a7e3870fb409c020 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 4 Jul 2026 05:54:50 +0530 Subject: [PATCH 05/12] test: fix conftest import locations and update readme links --- packages/toolbox-core/README.md | 17 +--------------- .../toolbox-core/src/toolbox_core/client.py | 5 +++-- .../src/toolbox_core/sync_client.py | 5 ++++- packages/toolbox-langchain/tests/test_e2e.py | 20 +++++++++++++++++++ packages/toolbox-llamaindex/tests/test_e2e.py | 20 +++++++++++++++++++ 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 77c008d74..b7f80aa0a 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -63,22 +63,7 @@ The core package provides a framework-agnostic way to interact with your Toolbox - [Authenticating Tools](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#authenticating-tools) - [Binding Parameter Values](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#parameter-binding) - [OpenTelemetry](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#opentelemetry) -- [Protocol Negotiation](#protocol-negotiation) - -## Protocol Negotiation - -By default, the client negotiates the newest protocol version supported by the server. You can provide a custom list of supported protocols to restrict negotiation to specific versions or a single version. Ensure you pass the [`Protocol`](src/toolbox_core/protocol.py) enum constants. Both `Protocol.MCP_LATEST` and `Protocol.MCP_DRAFT` are supported as well. - -```py -from toolbox_core import ToolboxClient, Protocol - -async def main(): - async with ToolboxClient( - "http://127.0.0.1:5000", - protocol=[Protocol.MCP_LATEST, Protocol.MCP_DRAFT] - ) as toolbox: - pass -``` +- [Protocol Negotiation](https://mcp-toolbox.dev/documentation/connect-to/toolbox-sdks/python-sdk/core/#protocol-negotiation) # Contributing diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index ab427b640..b5017a34c 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -218,8 +218,9 @@ def __init__( client_headers: Headers to include in each request sent through this client. protocol: The communication protocol to use. Can be a single version or a list of versions. - If a single version is provided (e.g. Protocol.MCP_LATEST), the client falls back through all older versions during negotiation. - If a list of versions is provided (e.g. [Protocol.MCP_LATEST, "2025-11-25"]), negotiation is restricted to those versions, and the newest version in the list is negotiated for first. + If no version is given, the latest version is tried for. + If a single version is given, then that version is tried for, followed by newest mutually supported version. + If a list of versions is provided (e.g. [Protocol.MCP_LATEST, "2025-11-25"]), negotiation is restricted to those versions, and the newest mutually supported version in that range is tried for. client_name: Optional client name for identification. client_version: Optional client version for identification. telemetry_enabled: Whether to enable OpenTelemetry tracing and metrics. (Default: False) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index cad63b2a7..d171638da 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -53,7 +53,10 @@ def __init__( Args: url: The base URL for the Toolbox service API (e.g., "http://localhost:5000"). client_headers: Headers to include in each request sent through this client. - protocol: The communication protocol to use. + protocol: The communication protocol to use. Can be a single version or a list of versions. + If no version is given, the latest version is tried for. + If a single version is given, then that version is tried for, followed by newest mutually supported version. + If a list of versions is provided (e.g. [Protocol.MCP_LATEST, "2025-11-25"]), negotiation is restricted to those versions, and the newest mutually supported version in that range is tried for. client_name: Optional client name for identification. client_version: Optional client version for identification. telemetry_enabled: Whether to enable OpenTelemetry tracing and metrics. (Default: False) diff --git a/packages/toolbox-langchain/tests/test_e2e.py b/packages/toolbox-langchain/tests/test_e2e.py index 7ebcf9698..da015f28c 100644 --- a/packages/toolbox-langchain/tests/test_e2e.py +++ b/packages/toolbox-langchain/tests/test_e2e.py @@ -97,6 +97,16 @@ async def test_aload_toolset_all(self, toolbox): name = tool._ToolboxTool__core_tool.__name__ assert name in tool_names + async def test_aload_toolset_explicit_protocol(self): + from toolbox_core.protocol import Protocol + + toolbox = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 + ) + toolset = await toolbox.aload_toolset() + assert len(toolset) == 7 + toolbox.close() + async def test_run_tool_async(self, get_n_rows_tool): response = await get_n_rows_tool.ainvoke({"num_rows": "2"}) @@ -244,6 +254,16 @@ def test_aload_toolset_all(self, toolbox): name = tool._ToolboxTool__core_tool.__name__ assert name in tool_names + def test_load_toolset_explicit_protocol(self): + from toolbox_core.protocol import Protocol + + toolbox = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 + ) + toolset = toolbox.load_toolset() + assert len(toolset) == 7 + toolbox.close() + @pytest.mark.asyncio async def test_run_tool_async(self, get_n_rows_tool): response = await get_n_rows_tool.ainvoke({"num_rows": "2"}) diff --git a/packages/toolbox-llamaindex/tests/test_e2e.py b/packages/toolbox-llamaindex/tests/test_e2e.py index 9f07a8e1f..7e80c8e2d 100644 --- a/packages/toolbox-llamaindex/tests/test_e2e.py +++ b/packages/toolbox-llamaindex/tests/test_e2e.py @@ -97,6 +97,16 @@ async def test_aload_toolset_all(self, toolbox): name = tool._ToolboxTool__core_tool.__name__ assert name in tool_names + async def test_aload_toolset_explicit_protocol(self): + from toolbox_core.protocol import Protocol + + toolbox = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 + ) + toolset = await toolbox.aload_toolset() + assert len(toolset) == 7 + toolbox.close() + async def test_run_tool_async(self, get_n_rows_tool): response = await get_n_rows_tool.acall(num_rows="2") @@ -244,6 +254,16 @@ def test_aload_toolset_all(self, toolbox): name = tool._ToolboxTool__core_tool.__name__ assert name in tool_names + def test_load_toolset_explicit_protocol(self): + from toolbox_core.protocol import Protocol + + toolbox = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 + ) + toolset = toolbox.load_toolset() + assert len(toolset) == 7 + toolbox.close() + @pytest.mark.asyncio async def test_run_tool_async(self, get_n_rows_tool): response = await get_n_rows_tool.acall(num_rows="2") From c026945fae87c125ab5ab9c482220405e9a0af60 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 4 Jul 2026 06:54:36 +0530 Subject: [PATCH 06/12] refactor: consolidate fallback mechanism tests into test_client.py and remove redundant test_fallback.py --- packages/toolbox-core/tests/test_client.py | 105 +++++++++++++++++ packages/toolbox-core/tests/test_fallback.py | 112 ------------------- 2 files changed, 105 insertions(+), 112 deletions(-) delete mode 100644 packages/toolbox-core/tests/test_fallback.py diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index f2cdf7157..f5b291a91 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -855,3 +855,108 @@ def test_toolbox_client_custom_protocols_invalid(): with pytest.raises(ValueError, match="Invalid protocol version 'invalid-version'"): ToolboxClient(TOOLBOX_SERVER_URL_STABLE, protocol=["invalid-version"]) + + +@pytest.mark.asyncio +async def test_artificial_array(): + """The Artificial Array Test: simulate server returning 2025-03-26, should fallback to 2024-11-05.""" + proxy = _McpTransportProxy( + "http://mock", + None, + Protocol.MCP_DRAFT, + None, + None, + False, + [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value], + ) + + proxy._active_transport.tool_get = AsyncMock( + side_effect=ProtocolNegotiationError(Protocol.MCP_v20250326.value) + ) + + with patch.object(proxy, "_create_transport") as mock_create: + mock_new_transport = AsyncMock() + mock_new_transport.tool_get.return_value = "success" + mock_create.return_value = mock_new_transport + + res = await proxy.tool_get("mock") + + assert res == "success" + mock_create.assert_called_with(Protocol.MCP_v20241105) + + +@pytest.mark.asyncio +async def test_cascading_fallback(): + """The Cascading Fallback Test: simulate server stateless generic error which throws the next stateful version.""" + proxy = _McpTransportProxy( + "http://mock", + None, + Protocol.MCP_DRAFT, + None, + None, + False, + [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value], + ) + + proxy._active_transport.tool_get = AsyncMock( + side_effect=ProtocolNegotiationError(Protocol.MCP_v20251125.value) + ) + + with patch.object(proxy, "_create_transport") as mock_create: + mock_new_transport = AsyncMock() + mock_new_transport.tool_get.return_value = "success" + mock_create.return_value = mock_new_transport + + res = await proxy.tool_get("mock") + + assert res == "success" + mock_create.assert_called_with(Protocol.MCP_v20251125) + + +@pytest.mark.asyncio +async def test_strict_constraint(): + """The Strict Constraint Test: simulate legacy server returning an unsupported old version.""" + proxy = _McpTransportProxy( + "http://mock", + None, + Protocol.MCP_DRAFT, + None, + None, + False, + [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value], + ) + + proxy._active_transport.tool_get = AsyncMock( + side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value) + ) + + with pytest.raises(RuntimeError, match="No mutually supported protocol version"): + await proxy.tool_get("mock") + + +@pytest.mark.asyncio +async def test_modern_smart_fallback(): + """The Modern Smart-Fallback Test: simulate modern payload correctly returning pre-intersected fallback.""" + proxy = _McpTransportProxy( + "http://mock", + None, + Protocol.MCP_DRAFT, + None, + None, + False, + [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value], + ) + + proxy._active_transport.tool_get = AsyncMock( + side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value) + ) + + with patch.object(proxy, "_create_transport") as mock_create: + mock_new_transport = AsyncMock() + mock_new_transport.tool_get.return_value = "success" + mock_create.return_value = mock_new_transport + + res = await proxy.tool_get("mock") + + assert res == "success" + mock_create.assert_called_with(Protocol.MCP_v20241105) diff --git a/packages/toolbox-core/tests/test_fallback.py b/packages/toolbox-core/tests/test_fallback.py deleted file mode 100644 index f3be223fd..000000000 --- a/packages/toolbox-core/tests/test_fallback.py +++ /dev/null @@ -1,112 +0,0 @@ -from unittest.mock import AsyncMock, patch - -import pytest - -from toolbox_core.client import _McpTransportProxy -from toolbox_core.exceptions import ProtocolNegotiationError -from toolbox_core.protocol import Protocol - - -@pytest.mark.asyncio -async def test_artificial_array(): - """The Artificial Array Test: simulate server returning 2025-03-26, should fallback to 2024-11-05.""" - proxy = _McpTransportProxy( - "http://mock", - None, - Protocol.MCP_DRAFT, - None, - None, - False, - [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value], - ) - - proxy._active_transport.tool_get = AsyncMock( - side_effect=ProtocolNegotiationError(Protocol.MCP_v20250326.value) - ) - - with patch.object(proxy, "_create_transport") as mock_create: - mock_new_transport = AsyncMock() - mock_new_transport.tool_get.return_value = "success" - mock_create.return_value = mock_new_transport - - res = await proxy.tool_get("mock") - - assert res == "success" - mock_create.assert_called_with(Protocol.MCP_v20241105) - - -@pytest.mark.asyncio -async def test_cascading_fallback(): - """The Cascading Fallback Test: simulate server stateless generic error which throws the next stateful version.""" - proxy = _McpTransportProxy( - "http://mock", - None, - Protocol.MCP_DRAFT, - None, - None, - False, - [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value], - ) - - proxy._active_transport.tool_get = AsyncMock( - side_effect=ProtocolNegotiationError(Protocol.MCP_v20251125.value) - ) - - with patch.object(proxy, "_create_transport") as mock_create: - mock_new_transport = AsyncMock() - mock_new_transport.tool_get.return_value = "success" - mock_create.return_value = mock_new_transport - - res = await proxy.tool_get("mock") - - assert res == "success" - mock_create.assert_called_with(Protocol.MCP_v20251125) - - -@pytest.mark.asyncio -async def test_strict_constraint(): - """The Strict Constraint Test: simulate legacy server returning an unsupported old version.""" - proxy = _McpTransportProxy( - "http://mock", - None, - Protocol.MCP_DRAFT, - None, - None, - False, - [Protocol.MCP_DRAFT.value, Protocol.MCP_v20251125.value], - ) - - proxy._active_transport.tool_get = AsyncMock( - side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value) - ) - - with pytest.raises(RuntimeError, match="No mutually supported protocol version"): - await proxy.tool_get("mock") - - -@pytest.mark.asyncio -async def test_modern_smart_fallback(): - """The Modern Smart-Fallback Test: simulate modern payload correctly returning pre-intersected fallback.""" - proxy = _McpTransportProxy( - "http://mock", - None, - Protocol.MCP_DRAFT, - None, - None, - False, - [Protocol.MCP_DRAFT.value, Protocol.MCP_v20241105.value], - ) - - proxy._active_transport.tool_get = AsyncMock( - side_effect=ProtocolNegotiationError(Protocol.MCP_v20241105.value) - ) - - with patch.object(proxy, "_create_transport") as mock_create: - mock_new_transport = AsyncMock() - mock_new_transport.tool_get.return_value = "success" - mock_create.return_value = mock_new_transport - - res = await proxy.tool_get("mock") - - assert res == "success" - mock_create.assert_called_with(Protocol.MCP_v20241105) From 5253ff94f23245e1bf39a561eb5b2e33c4cd3632 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 6 Jul 2026 13:08:25 +0530 Subject: [PATCH 07/12] chore: delint --- packages/toolbox-core/src/toolbox_core/sync_client.py | 2 +- packages/toolbox-core/tests/test_client.py | 3 ++- .../src/toolbox_langchain/async_client.py | 2 +- .../toolbox-langchain/src/toolbox_langchain/client.py | 2 +- packages/toolbox-langchain/tests/test_e2e.py | 8 ++------ .../src/toolbox_llamaindex/async_client.py | 2 +- .../toolbox-llamaindex/src/toolbox_llamaindex/client.py | 2 +- packages/toolbox-llamaindex/tests/test_e2e.py | 8 ++------ 8 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index d171638da..e438fd503 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -42,7 +42,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol = Protocol.MCP, + protocol: Union[Protocol, list[Protocol], list[str]] = Protocol.MCP, client_name: Optional[str] = None, client_version: Optional[str] = None, telemetry_enabled: bool = False, diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index f5b291a91..09b56dffe 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -22,7 +22,8 @@ from aiohttp import web from tests.constants import TOOLBOX_SERVER_URL_STABLE -from toolbox_core.client import ToolboxClient +from toolbox_core.client import ToolboxClient, _McpTransportProxy +from toolbox_core.exceptions import ProtocolNegotiationError from toolbox_core.itransport import ITransport from toolbox_core.protocol import ( ManifestSchema, diff --git a/packages/toolbox-langchain/src/toolbox_langchain/async_client.py b/packages/toolbox-langchain/src/toolbox_langchain/async_client.py index d5035b966..eae814e16 100644 --- a/packages/toolbox-langchain/src/toolbox_langchain/async_client.py +++ b/packages/toolbox-langchain/src/toolbox_langchain/async_client.py @@ -35,7 +35,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol | Sequence[Protocol] = Protocol.MCP, + protocol: Union[Protocol, list[Protocol], list[str]] = Protocol.MCP, telemetry_enabled: bool = False, ): """ diff --git a/packages/toolbox-langchain/src/toolbox_langchain/client.py b/packages/toolbox-langchain/src/toolbox_langchain/client.py index 677a0df02..c35b61f7f 100644 --- a/packages/toolbox-langchain/src/toolbox_langchain/client.py +++ b/packages/toolbox-langchain/src/toolbox_langchain/client.py @@ -31,7 +31,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol | Sequence[Protocol] = Protocol.MCP, + protocol: Union[Protocol, list[Protocol], list[str]] = Protocol.MCP, telemetry_enabled: bool = False, ) -> None: """ diff --git a/packages/toolbox-langchain/tests/test_e2e.py b/packages/toolbox-langchain/tests/test_e2e.py index da015f28c..4d4060b64 100644 --- a/packages/toolbox-langchain/tests/test_e2e.py +++ b/packages/toolbox-langchain/tests/test_e2e.py @@ -100,9 +100,7 @@ async def test_aload_toolset_all(self, toolbox): async def test_aload_toolset_explicit_protocol(self): from toolbox_core.protocol import Protocol - toolbox = ToolboxClient( - TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 - ) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL, protocol=Protocol.MCP_v20251125) toolset = await toolbox.aload_toolset() assert len(toolset) == 7 toolbox.close() @@ -257,9 +255,7 @@ def test_aload_toolset_all(self, toolbox): def test_load_toolset_explicit_protocol(self): from toolbox_core.protocol import Protocol - toolbox = ToolboxClient( - TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 - ) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL, protocol=Protocol.MCP_v20251125) toolset = toolbox.load_toolset() assert len(toolset) == 7 toolbox.close() diff --git a/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py b/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py index 58bb5b142..08ebc3ec5 100644 --- a/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py +++ b/packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py @@ -35,7 +35,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol | Sequence[Protocol] = Protocol.MCP, + protocol: Union[Protocol, list[Protocol], list[str]] = Protocol.MCP, telemetry_enabled: bool = False, ): """ diff --git a/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py b/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py index e653c2984..b6c9d0db3 100644 --- a/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py +++ b/packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py @@ -32,7 +32,7 @@ def __init__( client_headers: Optional[ Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] ] = None, - protocol: Protocol | Sequence[Protocol] = Protocol.MCP, + protocol: Union[Protocol, list[Protocol], list[str]] = Protocol.MCP, telemetry_enabled: bool = False, ) -> None: """ diff --git a/packages/toolbox-llamaindex/tests/test_e2e.py b/packages/toolbox-llamaindex/tests/test_e2e.py index 7e80c8e2d..21ebdb7f4 100644 --- a/packages/toolbox-llamaindex/tests/test_e2e.py +++ b/packages/toolbox-llamaindex/tests/test_e2e.py @@ -100,9 +100,7 @@ async def test_aload_toolset_all(self, toolbox): async def test_aload_toolset_explicit_protocol(self): from toolbox_core.protocol import Protocol - toolbox = ToolboxClient( - TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 - ) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL, protocol=Protocol.MCP_v20251125) toolset = await toolbox.aload_toolset() assert len(toolset) == 7 toolbox.close() @@ -257,9 +255,7 @@ def test_aload_toolset_all(self, toolbox): def test_load_toolset_explicit_protocol(self): from toolbox_core.protocol import Protocol - toolbox = ToolboxClient( - TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 - ) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL, protocol=Protocol.MCP_v20251125) toolset = toolbox.load_toolset() assert len(toolset) == 7 toolbox.close() From 9cd384e5787362792b3d7eb4e9d5e1504a21f5a2 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 6 Jul 2026 15:38:24 +0530 Subject: [PATCH 08/12] refactor: centralize test constants into package-specific files and update imports across test suites --- packages/toolbox-adk/tests/constants.py | 16 ++++++++++++++ .../toolbox-adk/tests/integration/conftest.py | 3 +-- packages/toolbox-langchain/tests/conftest.py | 3 +-- packages/toolbox-langchain/tests/constants.py | 16 ++++++++++++++ packages/toolbox-langchain/tests/test_e2e.py | 22 ++++++++----------- packages/toolbox-llamaindex/tests/conftest.py | 3 +-- .../toolbox-llamaindex/tests/constants.py | 16 ++++++++++++++ packages/toolbox-llamaindex/tests/test_e2e.py | 22 ++++++++----------- 8 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 packages/toolbox-adk/tests/constants.py create mode 100644 packages/toolbox-langchain/tests/constants.py create mode 100644 packages/toolbox-llamaindex/tests/constants.py diff --git a/packages/toolbox-adk/tests/constants.py b/packages/toolbox-adk/tests/constants.py new file mode 100644 index 000000000..701f830e6 --- /dev/null +++ b/packages/toolbox-adk/tests/constants.py @@ -0,0 +1,16 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" +TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" diff --git a/packages/toolbox-adk/tests/integration/conftest.py b/packages/toolbox-adk/tests/integration/conftest.py index 5c672547a..22254e4cb 100644 --- a/packages/toolbox-adk/tests/integration/conftest.py +++ b/packages/toolbox-adk/tests/integration/conftest.py @@ -29,8 +29,7 @@ from google.auth import compute_engine from google.cloud import secretmanager, storage -TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" -TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" +from tests.constants import TOOLBOX_SERVER_URL_DRAFT, TOOLBOX_SERVER_URL_STABLE #### Define Utility Functions diff --git a/packages/toolbox-langchain/tests/conftest.py b/packages/toolbox-langchain/tests/conftest.py index 05d9a175a..58cf686d2 100644 --- a/packages/toolbox-langchain/tests/conftest.py +++ b/packages/toolbox-langchain/tests/conftest.py @@ -29,8 +29,7 @@ from google.auth import compute_engine from google.cloud import secretmanager, storage -TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" -TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" +from tests.constants import TOOLBOX_SERVER_URL_DRAFT, TOOLBOX_SERVER_URL_STABLE #### Define Utility Functions diff --git a/packages/toolbox-langchain/tests/constants.py b/packages/toolbox-langchain/tests/constants.py new file mode 100644 index 000000000..701f830e6 --- /dev/null +++ b/packages/toolbox-langchain/tests/constants.py @@ -0,0 +1,16 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" +TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" diff --git a/packages/toolbox-langchain/tests/test_e2e.py b/packages/toolbox-langchain/tests/test_e2e.py index 4d4060b64..36a2db582 100644 --- a/packages/toolbox-langchain/tests/test_e2e.py +++ b/packages/toolbox-langchain/tests/test_e2e.py @@ -37,17 +37,13 @@ import pytest import pytest_asyncio from pydantic import ValidationError +from toolbox_core.protocol import Protocol +from tests.constants import TOOLBOX_SERVER_URL_STABLE from toolbox_langchain.client import ToolboxClient pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") -TOOLBOX_SERVER_URL = "http://localhost:5000" - -pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") - -pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") - @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") @@ -55,7 +51,7 @@ class TestE2EClientAsync: @pytest.fixture(scope="function") def toolbox(self): """Provides a ToolboxClient instance for each test.""" - toolbox = ToolboxClient(TOOLBOX_SERVER_URL) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL_STABLE) return toolbox @pytest_asyncio.fixture(scope="function") @@ -98,9 +94,9 @@ async def test_aload_toolset_all(self, toolbox): assert name in tool_names async def test_aload_toolset_explicit_protocol(self): - from toolbox_core.protocol import Protocol - - toolbox = ToolboxClient(TOOLBOX_SERVER_URL, protocol=Protocol.MCP_v20251125) + toolbox = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 + ) toolset = await toolbox.aload_toolset() assert len(toolset) == 7 toolbox.close() @@ -253,9 +249,9 @@ def test_aload_toolset_all(self, toolbox): assert name in tool_names def test_load_toolset_explicit_protocol(self): - from toolbox_core.protocol import Protocol - - toolbox = ToolboxClient(TOOLBOX_SERVER_URL, protocol=Protocol.MCP_v20251125) + toolbox = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 + ) toolset = toolbox.load_toolset() assert len(toolset) == 7 toolbox.close() diff --git a/packages/toolbox-llamaindex/tests/conftest.py b/packages/toolbox-llamaindex/tests/conftest.py index fa9784f1f..c505933e3 100644 --- a/packages/toolbox-llamaindex/tests/conftest.py +++ b/packages/toolbox-llamaindex/tests/conftest.py @@ -29,8 +29,7 @@ from google.auth import compute_engine from google.cloud import secretmanager, storage -TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" -TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" +from tests.constants import TOOLBOX_SERVER_URL_DRAFT, TOOLBOX_SERVER_URL_STABLE #### Define Utility Functions diff --git a/packages/toolbox-llamaindex/tests/constants.py b/packages/toolbox-llamaindex/tests/constants.py new file mode 100644 index 000000000..701f830e6 --- /dev/null +++ b/packages/toolbox-llamaindex/tests/constants.py @@ -0,0 +1,16 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" +TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" diff --git a/packages/toolbox-llamaindex/tests/test_e2e.py b/packages/toolbox-llamaindex/tests/test_e2e.py index 21ebdb7f4..553867ef9 100644 --- a/packages/toolbox-llamaindex/tests/test_e2e.py +++ b/packages/toolbox-llamaindex/tests/test_e2e.py @@ -37,17 +37,13 @@ import pytest import pytest_asyncio from pydantic import ValidationError +from toolbox_core.protocol import Protocol +from tests.constants import TOOLBOX_SERVER_URL_STABLE from toolbox_llamaindex.client import ToolboxClient pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") -TOOLBOX_SERVER_URL = "http://localhost:5000" - -pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") - -pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") - @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") @@ -55,7 +51,7 @@ class TestE2EClientAsync: @pytest.fixture(scope="function") def toolbox(self): """Provides a ToolboxClient instance for each test.""" - toolbox = ToolboxClient(TOOLBOX_SERVER_URL) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL_STABLE) return toolbox @pytest_asyncio.fixture(scope="function") @@ -98,9 +94,9 @@ async def test_aload_toolset_all(self, toolbox): assert name in tool_names async def test_aload_toolset_explicit_protocol(self): - from toolbox_core.protocol import Protocol - - toolbox = ToolboxClient(TOOLBOX_SERVER_URL, protocol=Protocol.MCP_v20251125) + toolbox = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 + ) toolset = await toolbox.aload_toolset() assert len(toolset) == 7 toolbox.close() @@ -253,9 +249,9 @@ def test_aload_toolset_all(self, toolbox): assert name in tool_names def test_load_toolset_explicit_protocol(self): - from toolbox_core.protocol import Protocol - - toolbox = ToolboxClient(TOOLBOX_SERVER_URL, protocol=Protocol.MCP_v20251125) + toolbox = ToolboxClient( + TOOLBOX_SERVER_URL_STABLE, protocol=Protocol.MCP_v20251125 + ) toolset = toolbox.load_toolset() assert len(toolset) == 7 toolbox.close() From c5fa7c7eddce825a8bd2f9558b721e0c1f950dba Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 6 Jul 2026 15:41:10 +0530 Subject: [PATCH 09/12] test: update integration tests to use stable server URL constant --- packages/toolbox-adk/tests/integration/test_integration.py | 7 +++---- packages/toolbox-langchain/tests/test_e2e.py | 2 +- packages/toolbox-llamaindex/tests/test_e2e.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/toolbox-adk/tests/integration/test_integration.py b/packages/toolbox-adk/tests/integration/test_integration.py index 739eb79a4..43837d574 100644 --- a/packages/toolbox-adk/tests/integration/test_integration.py +++ b/packages/toolbox-adk/tests/integration/test_integration.py @@ -32,8 +32,7 @@ from toolbox_core.protocol import Protocol from toolbox_adk import CredentialStrategy, ToolboxTool, ToolboxToolset - -TOOLBOX_SERVER_URL = "http://localhost:5000" +from tests.constants import TOOLBOX_SERVER_URL_STABLE pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") @@ -56,7 +55,7 @@ async def test_load_toolset_and_run(self): # Auth: TOOLBOX_IDENTITY for simplicity in this local test as we don't have ADK identity setup. toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name="my-toolset", credentials=CredentialStrategy.toolbox_identity(), ) @@ -90,7 +89,7 @@ async def test_load_toolset_and_run(self): async def test_load_toolset_with_default_protocol(self): """Test initializing toolset with default protocol (MCP).""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name="my-toolset", credentials=CredentialStrategy.toolbox_identity(), ) diff --git a/packages/toolbox-langchain/tests/test_e2e.py b/packages/toolbox-langchain/tests/test_e2e.py index 36a2db582..22f216d18 100644 --- a/packages/toolbox-langchain/tests/test_e2e.py +++ b/packages/toolbox-langchain/tests/test_e2e.py @@ -206,7 +206,7 @@ class TestE2EClientSync: @pytest.fixture(scope="session") def toolbox(self): """Provides a ToolboxClient instance for each test.""" - toolbox = ToolboxClient(TOOLBOX_SERVER_URL) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL_STABLE) return toolbox @pytest.fixture(scope="function") diff --git a/packages/toolbox-llamaindex/tests/test_e2e.py b/packages/toolbox-llamaindex/tests/test_e2e.py index 553867ef9..225f66666 100644 --- a/packages/toolbox-llamaindex/tests/test_e2e.py +++ b/packages/toolbox-llamaindex/tests/test_e2e.py @@ -206,7 +206,7 @@ class TestE2EClientSync: @pytest.fixture(scope="session") def toolbox(self): """Provides a ToolboxClient instance for each test.""" - toolbox = ToolboxClient(TOOLBOX_SERVER_URL) + toolbox = ToolboxClient(TOOLBOX_SERVER_URL_STABLE) return toolbox @pytest.fixture(scope="function") From 207c4e34ae35192913bb05e2dc99e985e9dee7c1 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 6 Jul 2026 15:42:21 +0530 Subject: [PATCH 10/12] refactor: remove redundant pytestmark fixture declaration in integration tests --- packages/toolbox-adk/tests/integration/test_integration.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/toolbox-adk/tests/integration/test_integration.py b/packages/toolbox-adk/tests/integration/test_integration.py index 43837d574..01474f0d7 100644 --- a/packages/toolbox-adk/tests/integration/test_integration.py +++ b/packages/toolbox-adk/tests/integration/test_integration.py @@ -36,8 +36,6 @@ pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") -pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") - # Ensure TOOLBOX_VERSION is set for the fixture if "TOOLBOX_VERSION" not in os.environ: os.environ["TOOLBOX_VERSION"] = "0.0.1" # Use a valid version or mock From 2c713c715db2fd43a535bfbd535e50455986395e Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 6 Jul 2026 15:43:30 +0530 Subject: [PATCH 11/12] test: update integration tests to use TOOLBOX_SERVER_URL_STABLE for server connections --- .../tests/integration/test_integration.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/toolbox-adk/tests/integration/test_integration.py b/packages/toolbox-adk/tests/integration/test_integration.py index 01474f0d7..867bd76b2 100644 --- a/packages/toolbox-adk/tests/integration/test_integration.py +++ b/packages/toolbox-adk/tests/integration/test_integration.py @@ -111,7 +111,7 @@ async def test_load_toolset_with_default_protocol(self): async def test_load_toolset_with_explicit_protocol(self): """Test initializing toolset with specific protocol (MCP_v20251125).""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name="my-toolset", credentials=CredentialStrategy.toolbox_identity(), protocol=Protocol.MCP_v20251125, @@ -135,7 +135,7 @@ async def test_load_toolset_with_explicit_protocol(self): async def test_partial_loading_by_names(self): toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-n-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -155,7 +155,7 @@ async def test_partial_loading_by_names(self): async def test_bound_params_e2e(self): # Test binding param at toolset level toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-n-rows"], bound_params={"num_rows": "2"}, credentials=CredentialStrategy.toolbox_identity(), @@ -171,7 +171,7 @@ async def test_bound_params_e2e(self): async def test_3lo_flow_simulation(self): toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, # Load a specific tool that we know the arguments for tool_names=["get-n-rows"], credentials=CredentialStrategy.user_identity( @@ -268,7 +268,7 @@ async def test_3lo_flow_simulation(self): async def test_manual_token_integration(self): """Test the MANUAL_TOKEN strategy.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name="my-toolset", credentials=CredentialStrategy.manual_token(token="fake-manual-token"), ) @@ -287,7 +287,7 @@ async def test_manual_credentials_integration(self): mock_creds.token = "fake-creds-token" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name="my-toolset", credentials=CredentialStrategy.manual_credentials(credentials=mock_creds), ) @@ -300,7 +300,7 @@ async def test_manual_credentials_integration(self): async def test_api_key_integration(self): """Test the API_KEY strategy.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name="my-toolset", credentials=CredentialStrategy.api_key(key="my-key", header_name="x-foo"), ) @@ -336,7 +336,7 @@ async def test_adk_integration_optional_params(self): # 3. Use in Toolset toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name="my-toolset", credentials=strategy, ) @@ -360,7 +360,7 @@ async def test_header_collision(self): creds_token = "Bearer strategy-token" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name="my-toolset", additional_headers={"Authorization": manual_override}, credentials=CredentialStrategy.manual_token(token="strategy-token"), @@ -394,7 +394,7 @@ async def test_load_toolset_specific( ): """Load a specific toolset""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, toolset_name=toolset_name, credentials=CredentialStrategy.toolbox_identity(), ) @@ -409,7 +409,7 @@ async def test_load_toolset_specific( async def test_load_toolset_default(self): """Load the default toolset, i.e. all tools.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, credentials=CredentialStrategy.toolbox_identity(), ) try: @@ -432,7 +432,7 @@ async def test_load_toolset_default(self): async def test_run_tool(self): """Invoke a tool.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-n-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -454,7 +454,7 @@ async def test_run_tool(self): async def test_run_tool_missing_params(self): """Invoke a tool with missing params.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-n-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -473,7 +473,7 @@ async def test_run_tool_missing_params(self): async def test_run_tool_wrong_param_type(self): """Invoke a tool with wrong param type.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-n-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -497,7 +497,7 @@ class TestBindParams: async def test_bind_params(self): """Bind a param to an existing tool.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-n-rows"], bound_params={"num_rows": "3"}, credentials=CredentialStrategy.toolbox_identity(), @@ -519,7 +519,7 @@ async def test_bind_params(self): async def test_bind_params_callable(self): """Bind a callable param to an existing tool.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-n-rows"], bound_params={"num_rows": lambda: "3"}, credentials=CredentialStrategy.toolbox_identity(), @@ -545,7 +545,7 @@ class TestAuth: async def test_run_tool_unauth_with_auth(self, auth_token2: str): """Tests running a tool that doesn't require auth, with auth provided.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-row-by-id"], auth_token_getters={"my-test-auth": lambda: auth_token2}, credentials=CredentialStrategy.toolbox_identity(), @@ -562,7 +562,7 @@ async def test_run_tool_unauth_with_auth(self, auth_token2: str): async def test_run_multiple_tools_unauth_with_auth(self, auth_token2: str): """Tests running multiple tools that don't require auth, verifying formatting of tool lists.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-row-by-id", "search-rows"], auth_token_getters={"my-test-auth": lambda: auth_token2}, credentials=CredentialStrategy.toolbox_identity(), @@ -579,7 +579,7 @@ async def test_run_multiple_tools_unauth_with_auth(self, auth_token2: str): async def test_run_multiple_tools_partial_auth_usage(self, auth_token2: str): """Tests that when some tokens are used and some aren't across diverse tools, only the truly unused tokens appear in the error.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=[ "get-row-by-id-auth", "search-rows", @@ -604,7 +604,7 @@ async def test_run_tool_no_auth(self): """Tests running a tool requiring auth without providing auth.""" # Note: We load it without auth getters. Invocation should fail. toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-row-by-id-auth"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -624,7 +624,7 @@ async def test_run_tool_no_auth(self): async def test_run_tool_wrong_auth(self, auth_token2: str): """Tests running a tool with incorrect auth.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-row-by-id-auth"], auth_token_getters={"my-test-auth": lambda: auth_token2}, credentials=CredentialStrategy.toolbox_identity(), @@ -645,7 +645,7 @@ async def test_run_tool_wrong_auth(self, auth_token2: str): async def test_run_tool_auth(self, auth_token1: str): """Tests running a tool with correct auth.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-row-by-id-auth"], auth_token_getters={"my-test-auth": lambda: auth_token1}, credentials=CredentialStrategy.toolbox_identity(), @@ -667,7 +667,7 @@ async def get_token_asynchronously(): return auth_token1 toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["get-row-by-id-auth"], auth_token_getters={"my-test-auth": get_token_asynchronously}, credentials=CredentialStrategy.toolbox_identity(), @@ -693,7 +693,7 @@ class TestOptionalParams: async def test_run_tool_with_optional_params_omitted(self): """Invoke a tool providing only the required parameter.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["search-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -712,7 +712,7 @@ async def test_run_tool_with_optional_params_omitted(self): async def test_run_tool_with_all_valid_params(self): """Invoke a tool providing all parameters.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["search-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -732,7 +732,7 @@ async def test_run_tool_with_all_valid_params(self): async def test_run_tool_with_missing_required_param(self): """Invoke a tool without its required parameter.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["search-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -757,7 +757,7 @@ class TestMapParams: async def test_run_tool_with_map_params(self): """Invoke a tool with valid map parameters.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["process-data"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -788,7 +788,7 @@ async def test_run_tool_with_map_params(self): async def test_run_tool_with_wrong_map_value_type(self): """Invoke a tool with a map parameter having the wrong value type.""" toolset = ToolboxToolset( - server_url=TOOLBOX_SERVER_URL, + server_url=TOOLBOX_SERVER_URL_STABLE, tool_names=["process-data"], credentials=CredentialStrategy.toolbox_identity(), ) From 93eb16f6b2b4d284a68ffacd79960eddf5be5c88 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 6 Jul 2026 19:02:24 +0530 Subject: [PATCH 12/12] chore: delint --- packages/toolbox-adk/tests/integration/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-adk/tests/integration/test_integration.py b/packages/toolbox-adk/tests/integration/test_integration.py index 867bd76b2..af1255de2 100644 --- a/packages/toolbox-adk/tests/integration/test_integration.py +++ b/packages/toolbox-adk/tests/integration/test_integration.py @@ -31,8 +31,8 @@ from pydantic import ValidationError from toolbox_core.protocol import Protocol -from toolbox_adk import CredentialStrategy, ToolboxTool, ToolboxToolset from tests.constants import TOOLBOX_SERVER_URL_STABLE +from toolbox_adk import CredentialStrategy, ToolboxTool, ToolboxToolset pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url")