From 308d19f6173481dba8d0f99062b125d1ff904642 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 06:03:12 +0530 Subject: [PATCH 01/14] feat(core): add custom protocols array support for ToolboxClient --- .../toolbox-core/src/toolbox_core/client.py | 23 +++++++++++++++++-- .../mcp_transport/transport_base.py | 2 ++ .../mcp_transport/v20250618/mcp.py | 2 +- .../mcp_transport/v20251125/mcp.py | 2 +- .../mcp_transport/v20260618/mcp.py | 4 ++-- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index b2224eedc..e05715bc1 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}") @@ -166,7 +173,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 +195,25 @@ 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") + supported_protocols = [ + p.value if isinstance(p, Protocol) else str(p) for p in protocol + ] + 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..1a70953eb 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,7 @@ 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..f7511e2e5 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,7 @@ async def _send_request( "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 ] @@ -131,7 +131,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 ] From 77c181e29a13b45e9de24f898403ec6f39aa85bd Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 06:13:25 +0530 Subject: [PATCH 02/14] fix(core): sort custom protocols array from newest to oldest --- packages/toolbox-core/src/toolbox_core/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index e05715bc1..dbc17ce22 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -198,9 +198,15 @@ def __init__( if isinstance(protocol, list): if not protocol: raise ValueError("protocol list cannot be empty") - supported_protocols = [ + user_protocols = { p.value if isinstance(p, Protocol) else str(p) for p in protocol + } + # Intersect with the globally sorted list to strictly enforce newest-to-oldest ordering + supported_protocols = [ + v for v in Protocol.get_supported_mcp_versions() if v in user_protocols ] + if not supported_protocols: + raise ValueError("No supported protocols found in the provided list") initial_protocol = Protocol(supported_protocols[0]) else: supported_protocols = None From e7bbb1749590743e7d987c932cdd854371f9258b Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 06:22:54 +0530 Subject: [PATCH 03/14] test(core): add unit tests for custom protocols array --- packages/toolbox-core/tests/test_client.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index e28bb03a1..96d1c60c2 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -828,3 +828,29 @@ def test_toolbox_client_no_warning_on_explicit_mcp_version(): "http://localhost:5000", 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( + "http://localhost:5000", + 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.""" + import pytest + with pytest.raises(ValueError, match="protocol list cannot be empty"): + ToolboxClient("http://localhost:5000", protocol=[]) + + with pytest.raises(ValueError, match="No supported protocols found"): + ToolboxClient("http://localhost:5000", protocol=["invalid-version"]) From dbc81948c7e8160d94bd76deead04adcd95c85a1 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 10:55:26 +0530 Subject: [PATCH 04/14] chore: delint --- .../src/toolbox_core/mcp_transport/transport_base.py | 4 +++- packages/toolbox-core/tests/test_client.py | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) 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 1a70953eb..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 @@ -51,7 +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._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/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index 96d1c60c2..fd73f260a 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -834,12 +834,12 @@ 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( - "http://localhost:5000", - protocol=[Protocol.MCP_v20241105, Protocol.MCP_DRAFT, "2025-06-18"] + "http://localhost:5000", + 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) @@ -849,8 +849,9 @@ def test_toolbox_client_custom_protocols(): def test_toolbox_client_custom_protocols_invalid(): """Test that custom protocols array raises error on invalid inputs.""" import pytest + with pytest.raises(ValueError, match="protocol list cannot be empty"): ToolboxClient("http://localhost:5000", protocol=[]) - + with pytest.raises(ValueError, match="No supported protocols found"): ToolboxClient("http://localhost:5000", protocol=["invalid-version"]) From f21d8a917565a3b11c1b05734e946a7904f237d2 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 11:25:46 +0530 Subject: [PATCH 05/14] docs: add protocol negotiation documentation --- packages/toolbox-core/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 60daf7580..715591586 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -58,7 +58,23 @@ 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) + +## 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` enum constants. + +```py +from toolbox_core import ToolboxClient, Protocol + +async def main(): + async with ToolboxClient( + "http://127.0.0.1:5000", + user_protocols=[Protocol.MCP_V20251125, Protocol.MCP_V20241105] + ) as toolbox: + pass +``` - [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) From 561478a32f88939692e294c0d6d9a4325983822f Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 11:30:17 +0530 Subject: [PATCH 06/14] style(core): run black and isort formatting --- packages/toolbox-core/src/toolbox_core/client.py | 15 ++++++++++++--- packages/toolbox-core/tests/test_client.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index dbc17ce22..faf8eeec1 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -198,12 +198,21 @@ def __init__( if isinstance(protocol, list): if not protocol: raise ValueError("protocol list cannot be empty") - user_protocols = { + 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 Protocol.get_supported_mcp_versions() if v in user_protocols + v for v in supported_mcp_versions if v in user_protocols_set ] if not supported_protocols: raise ValueError("No supported protocols found in the provided list") diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index fd73f260a..2edf01960 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -853,5 +853,5 @@ def test_toolbox_client_custom_protocols_invalid(): with pytest.raises(ValueError, match="protocol list cannot be empty"): ToolboxClient("http://localhost:5000", protocol=[]) - with pytest.raises(ValueError, match="No supported protocols found"): + with pytest.raises(ValueError, match="Invalid protocol version 'invalid-version'"): ToolboxClient("http://localhost:5000", protocol=["invalid-version"]) From 58cf118ec6e822c456c6b90dd3ccf060b2a0ca72 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 11:33:33 +0530 Subject: [PATCH 07/14] test(core): add strict integration tests for MCP_LATEST and MCP_DRAFT docs: update README to document MCP_LATEST and MCP_DRAFT --- packages/toolbox-core/README.md | 4 ++-- packages/toolbox-core/tests/test_e2e_mcp.py | 24 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 715591586..193c98214 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -63,7 +63,7 @@ The core package provides a framework-agnostic way to interact with your Toolbox ## 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` enum constants. +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` enum constants. Both `Protocol.MCP_LATEST` and `Protocol.MCP_DRAFT` are supported as well. ```py from toolbox_core import ToolboxClient, Protocol @@ -71,7 +71,7 @@ from toolbox_core import ToolboxClient, Protocol async def main(): async with ToolboxClient( "http://127.0.0.1:5000", - user_protocols=[Protocol.MCP_V20251125, Protocol.MCP_V20241105] + protocol=[Protocol.MCP_LATEST, Protocol.MCP_DRAFT] ) as toolbox: pass ``` diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index 7ffb3ddfa..ef237d67f 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -490,3 +490,27 @@ async def test_mcp_draft_fallback(): 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(): + """Verify that explicitly using MCP_LATEST works successfully.""" + async with ToolboxClient( + "http://localhost:5000", protocol=Protocol.MCP_LATEST + ) 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_custom_protocols_list(): + """Verify that passing a list of protocols with MCP_LATEST and MCP_DRAFT works successfully.""" + async with ToolboxClient( + "http://localhost:5000", protocol=[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 From ba02fe32da1e938fe828fbc2cd9da688ba1dc82c Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 11:35:00 +0530 Subject: [PATCH 08/14] test(core): verify negotiated protocol matches expectations in e2e tests --- packages/toolbox-core/tests/test_e2e_mcp.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index ef237d67f..ae1caed89 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -502,6 +502,10 @@ async def test_mcp_latest_protocol(): 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 @@ -514,3 +518,8 @@ async def test_mcp_custom_protocols_list(): tool = await client.load_tool("get-n-rows") response = await tool(num_rows="1") assert "row1" in response + # Server on :5000 doesn't support DRAFT, should fallback to LATEST + assert ( + client._ToolboxClient__transport._protocol_version + == Protocol.MCP_LATEST.value + ) From 67ae062aff795d660f30114d231391a04b287d39 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 11:48:03 +0530 Subject: [PATCH 09/14] test(core): refactor E2E tests to use semantic server URL constants --- packages/toolbox-core/tests/conftest.py | 12 +++--- packages/toolbox-core/tests/constants.py | 16 ++++++++ packages/toolbox-core/tests/test_client.py | 11 +++--- packages/toolbox-core/tests/test_e2e.py | 5 ++- packages/toolbox-core/tests/test_e2e_mcp.py | 40 +++++++++++--------- packages/toolbox-core/tests/test_sync_e2e.py | 3 +- 6 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 packages/toolbox-core/tests/constants.py diff --git a/packages/toolbox-core/tests/conftest.py b/packages/toolbox-core/tests/conftest.py index 2e500752e..823e0f86e 100644 --- a/packages/toolbox-core/tests/conftest.py +++ b/packages/toolbox-core/tests/conftest.py @@ -29,6 +29,8 @@ from google.auth import compute_engine from google.cloud import secretmanager, storage +from tests.constants import TOOLBOX_SERVER_URL_DRAFT, TOOLBOX_SERVER_URL_STABLE + #### Define Utility Functions def get_env_var(key: str) -> str: @@ -187,7 +189,7 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None @pytest.fixture( - params=["http://localhost:5000", "http://localhost:5001"], scope="session" + params=[TOOLBOX_SERVER_URL_STABLE, TOOLBOX_SERVER_URL_DRAFT], scope="session" ) def toolbox_server_url(request) -> str: return request.param @@ -201,13 +203,13 @@ def patch_toolbox_client_url(toolbox_server_url): original_init = ToolboxClient.__init__ original_sync_init = ToolboxSyncClient.__init__ - def new_init(self, url="http://localhost:5000", *args, **kwargs): - if url == "http://localhost:5000": + def new_init(self, url=TOOLBOX_SERVER_URL_STABLE, *args, **kwargs): + if url == TOOLBOX_SERVER_URL_STABLE: url = toolbox_server_url original_init(self, url, *args, **kwargs) - def new_sync_init(self, url="http://localhost:5000", *args, **kwargs): - if url == "http://localhost:5000": + def new_sync_init(self, url=TOOLBOX_SERVER_URL_STABLE, *args, **kwargs): + if url == TOOLBOX_SERVER_URL_STABLE: url = toolbox_server_url original_sync_init(self, url, *args, **kwargs) 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 2edf01960..5466760d0 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,7 +826,7 @@ 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 @@ -834,7 +835,7 @@ 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( - "http://localhost:5000", + TOOLBOX_SERVER_URL_STABLE, protocol=[Protocol.MCP_v20241105, Protocol.MCP_DRAFT, "2025-06-18"], ) mock_proxy.assert_called_once() @@ -851,7 +852,7 @@ def test_toolbox_client_custom_protocols_invalid(): import pytest with pytest.raises(ValueError, match="protocol list cannot be empty"): - ToolboxClient("http://localhost:5000", protocol=[]) + ToolboxClient(TOOLBOX_SERVER_URL_STABLE, protocol=[]) with pytest.raises(ValueError, match="Invalid protocol version 'invalid-version'"): - ToolboxClient("http://localhost:5000", protocol=["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 0b926e178..7ed28a3e9 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 @@ -28,7 +29,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: @@ -112,7 +113,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 ae1caed89..8d3cc01ac 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_STABLE from toolbox_core.client import ToolboxClient from toolbox_core.protocol import Protocol from toolbox_core.tool import ToolboxTool @@ -30,7 +31,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: @@ -100,8 +101,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: @@ -109,7 +110,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 @@ -472,9 +473,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 @@ -482,10 +483,10 @@ 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( - "http://localhost:5000", protocol=Protocol.MCP_DRAFT + toolbox_server_url, protocol=Protocol.MCP_DRAFT ) as client: tool = await client.load_tool("get-n-rows") response = await tool(num_rows="1") @@ -494,10 +495,10 @@ async def test_mcp_draft_fallback(): @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") -async def test_mcp_latest_protocol(): +async def test_mcp_latest_protocol(toolbox_server_url: str): """Verify that explicitly using MCP_LATEST works successfully.""" async with ToolboxClient( - "http://localhost:5000", protocol=Protocol.MCP_LATEST + toolbox_server_url, protocol=Protocol.MCP_LATEST ) as client: tool = await client.load_tool("get-n-rows") response = await tool(num_rows="1") @@ -510,16 +511,21 @@ async def test_mcp_latest_protocol(): @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") -async def test_mcp_custom_protocols_list(): +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_LATEST, Protocol.MCP_DRAFT] + toolbox_server_url, protocol=[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 - # Server on :5000 doesn't support DRAFT, should fallback to LATEST - assert ( - client._ToolboxClient__transport._protocol_version - == Protocol.MCP_LATEST.value - ) + 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_sync_e2e.py b/packages/toolbox-core/tests/test_sync_e2e.py index a306cedc2..4d26bc1e6 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 @@ -22,7 +23,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: From da892a426d5f6b8676501fc1d3d0929a2bb573f9 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 11:49:15 +0530 Subject: [PATCH 10/14] test(core): refactor E2E tests to use semantic server URL constants --- packages/toolbox-core/tests/test_e2e_mcp.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index 8d3cc01ac..d2f9ad198 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -19,7 +19,7 @@ import pytest_asyncio from pydantic import ValidationError -from tests.constants import TOOLBOX_SERVER_URL_STABLE +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 @@ -485,9 +485,7 @@ async def test_mcp_default_protocol(toolbox_server_url: str): @pytest.mark.usefixtures("toolbox_server") 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: + 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 From fcf49a71ec4a16568c9071bd518e1a1cbcbd7a96 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 11:57:53 +0530 Subject: [PATCH 11/14] docs(core): link to Protocol enum in README --- packages/toolbox-core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 193c98214..f8b23b29b 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -63,7 +63,7 @@ The core package provides a framework-agnostic way to interact with your Toolbox ## 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` enum constants. Both `Protocol.MCP_LATEST` and `Protocol.MCP_DRAFT` are supported as well. +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#L47) enum constants. Both `Protocol.MCP_LATEST` and `Protocol.MCP_DRAFT` are supported as well. ```py from toolbox_core import ToolboxClient, Protocol From 36c49942abd63cf4e50ea4962a56faed6184ea4a Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 12:07:48 +0530 Subject: [PATCH 12/14] docs(core): fix Protocol Negotiation TOC position and remove line numbers from links --- packages/toolbox-core/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index f8b23b29b..4f57e4a01 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -61,9 +61,14 @@ The core package provides a framework-agnostic way to interact with your Toolbox - [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#L47) enum constants. Both `Protocol.MCP_LATEST` and `Protocol.MCP_DRAFT` are supported as well. +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 @@ -75,10 +80,6 @@ async def main(): ) as toolbox: pass ``` -- [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) # Contributing From d05864350106a42b5d59975cbcb23cf545bf44a8 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 12:34:54 +0530 Subject: [PATCH 13/14] feat(orchestration): support sequence of protocols in langchain and llamaindex clients --- .../toolbox-langchain/src/toolbox_langchain/async_client.py | 3 ++- packages/toolbox-langchain/src/toolbox_langchain/client.py | 3 ++- .../toolbox-llamaindex/src/toolbox_llamaindex/async_client.py | 3 ++- packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) 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 6b4845a43e76d974b59a53753dce02d1fb7ce98c Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Thu, 2 Jul 2026 15:05:49 +0530 Subject: [PATCH 14/14] fix(core): correct protocol array order in fallback test --- packages/toolbox-core/tests/test_e2e_mcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index d2f9ad198..5a47e181c 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -512,7 +512,7 @@ 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, protocol=[Protocol.MCP_LATEST, Protocol.MCP_DRAFT] + toolbox_server_url, protocol=[Protocol.MCP_DRAFT, Protocol.MCP_LATEST] ) as client: tool = await client.load_tool("get-n-rows") response = await tool(num_rows="1")