From 607eb65ab7c2543e3f3743a632a8757c198f9a4c Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 27 Jan 2026 12:21:52 +0530 Subject: [PATCH 01/18] refactor(tests): refactor mcp tests --- .../mcp_transport/test_header_propagation.py | 110 ------------------ .../tests/mcp_transport/test_v20241105.py | 31 +++++ .../tests/mcp_transport/test_v20250326.py | 32 +++++ .../tests/mcp_transport/test_v20250618.py | 31 +++++ 4 files changed, 94 insertions(+), 110 deletions(-) delete mode 100644 packages/toolbox-core/tests/mcp_transport/test_header_propagation.py diff --git a/packages/toolbox-core/tests/mcp_transport/test_header_propagation.py b/packages/toolbox-core/tests/mcp_transport/test_header_propagation.py deleted file mode 100644 index ac902443..00000000 --- a/packages/toolbox-core/tests/mcp_transport/test_header_propagation.py +++ /dev/null @@ -1,110 +0,0 @@ -# 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. - -from unittest.mock import AsyncMock - -import pytest - -from toolbox_core.mcp_transport.v20241105 import types as types_v20241105 -from toolbox_core.mcp_transport.v20241105.mcp import McpHttpTransportV20241105 -from toolbox_core.mcp_transport.v20250326 import types as types_v20250326 -from toolbox_core.mcp_transport.v20250326.mcp import McpHttpTransportV20250326 -from toolbox_core.mcp_transport.v20250618 import types as types_v20250618 -from toolbox_core.mcp_transport.v20250618.mcp import McpHttpTransportV20250618 -from toolbox_core.protocol import Protocol - -TEST_CASES = [ - ( - McpHttpTransportV20241105, - Protocol.MCP_v20241105, - types_v20241105, - "2024-11-05", - ), - ( - McpHttpTransportV20250326, - Protocol.MCP_v20250326, - types_v20250326, - "2025-03-26", - ), - ( - McpHttpTransportV20250618, - Protocol.MCP_v20250618, - types_v20250618, - "2025-06-18", - ), -] - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "TransportClass, protocol_enum, types_module, protocol_version_str", TEST_CASES -) -async def test_ensure_initialized_passes_headers( - TransportClass, protocol_enum, types_module, protocol_version_str -): - mock_session = AsyncMock() - transport = TransportClass( - "http://fake.com", session=mock_session, protocol=protocol_enum - ) - - transport._initialize_session = AsyncMock() - - test_headers = {"X-Test": "123"} - await transport._ensure_initialized(headers=test_headers) - - transport._initialize_session.assert_called_with(headers=test_headers) - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "TransportClass, protocol_enum, types_module, protocol_version_str", TEST_CASES -) -async def test_initialize_passes_headers_to_request( - TransportClass, protocol_enum, types_module, protocol_version_str -): - mock_session = AsyncMock() - transport = TransportClass( - "http://fake.com", session=mock_session, protocol=protocol_enum - ) - - # Mock _send_request to simulate successful init - transport._send_request = AsyncMock() - transport._send_request.return_value = types_module.InitializeResult( - protocolVersion=protocol_version_str, - capabilities=types_module.ServerCapabilities(tools={"listChanged": True}), - serverInfo=types_module.Implementation(name="test", version="1.0"), - ) - - # Mock session ID injection which happens in _send_request usually, - # but here we just set it manually to satisfy the check for v20250326 - if isinstance(transport, McpHttpTransportV20250326): - transport._session_id = "test-session" - - test_headers = {"Authorization": "Bearer token"} - await transport._initialize_session(headers=test_headers) - - # Verify calls - assert transport._send_request.call_count == 2 - - # First call: InitializeRequest - init_call = transport._send_request.call_args_list[0] - assert isinstance(init_call.kwargs["request"], types_module.InitializeRequest) - assert init_call.kwargs["headers"] == test_headers - - # Second call: InitializedNotification - notify_call = transport._send_request.call_args_list[1] - assert isinstance( - notify_call.kwargs["request"], types_module.InitializedNotification - ) - assert notify_call.kwargs["headers"] == test_headers diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20241105.py b/packages/toolbox-core/tests/mcp_transport/test_v20241105.py index e290df2c..c1368877 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20241105.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20241105.py @@ -206,6 +206,37 @@ async def test_initialize_session_missing_tools_capability(self, transport, mock ): await transport._initialize_session() + async def test_ensure_initialized_passes_headers(self, transport): + transport._initialize_session = AsyncMock() + + test_headers = {"X-Test": "123"} + await transport._ensure_initialized(headers=test_headers) + + transport._initialize_session.assert_called_with(headers=test_headers) + + async def test_initialize_passes_headers_to_request(self, transport): + transport._send_request = AsyncMock() + transport._send_request.return_value = types.InitializeResult( + protocolVersion="2024-11-05", + capabilities=types.ServerCapabilities(tools={"listChanged": True}), + serverInfo=types.Implementation(name="test", version="1.0"), + ) + + test_headers = {"Authorization": "Bearer token"} + await transport._initialize_session(headers=test_headers) + + assert transport._send_request.call_count == 2 + + init_call = transport._send_request.call_args_list[0] + assert isinstance(init_call.kwargs["request"], types.InitializeRequest) + assert init_call.kwargs["headers"] == test_headers + + notify_call = transport._send_request.call_args_list[1] + assert isinstance( + notify_call.kwargs["request"], types.InitializedNotification + ) + assert notify_call.kwargs["headers"] == test_headers + # --- Tool Management Tests --- async def test_tools_list_success(self, transport, mocker): diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20250326.py b/packages/toolbox-core/tests/mcp_transport/test_v20250326.py index c777cc92..de0808f3 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20250326.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20250326.py @@ -210,6 +210,38 @@ async def test_initialize_session_missing_session_id(self, transport, mocker): ): await transport._initialize_session() + async def test_ensure_initialized_passes_headers(self, transport): + transport._initialize_session = AsyncMock() + + test_headers = {"X-Test": "123"} + await transport._ensure_initialized(headers=test_headers) + + transport._initialize_session.assert_called_with(headers=test_headers) + + async def test_initialize_passes_headers_to_request(self, transport): + transport._send_request = AsyncMock() + transport._send_request.return_value = types.InitializeResult( + protocolVersion="2025-03-26", + capabilities=types.ServerCapabilities(tools={"listChanged": True}), + serverInfo=types.Implementation(name="test", version="1.0"), + ) + transport._session_id = "test-session" + + test_headers = {"Authorization": "Bearer token"} + await transport._initialize_session(headers=test_headers) + + assert transport._send_request.call_count == 2 + + init_call = transport._send_request.call_args_list[0] + assert isinstance(init_call.kwargs["request"], types.InitializeRequest) + assert init_call.kwargs["headers"] == test_headers + + notify_call = transport._send_request.call_args_list[1] + assert isinstance( + notify_call.kwargs["request"], types.InitializedNotification + ) + assert notify_call.kwargs["headers"] == test_headers + # --- Tool Management Tests --- async def test_tools_list_success(self, transport, mocker): diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20250618.py b/packages/toolbox-core/tests/mcp_transport/test_v20250618.py index b98145e3..7c4b4d6c 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20250618.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20250618.py @@ -214,6 +214,37 @@ async def test_initialize_session_missing_tools_capability(self, transport, mock ): await transport._initialize_session() + async def test_ensure_initialized_passes_headers(self, transport): + transport._initialize_session = AsyncMock() + + test_headers = {"X-Test": "123"} + await transport._ensure_initialized(headers=test_headers) + + transport._initialize_session.assert_called_with(headers=test_headers) + + async def test_initialize_passes_headers_to_request(self, transport): + transport._send_request = AsyncMock() + transport._send_request.return_value = types.InitializeResult( + protocolVersion="2025-06-18", + capabilities=types.ServerCapabilities(tools={"listChanged": True}), + serverInfo=types.Implementation(name="test", version="1.0"), + ) + + test_headers = {"Authorization": "Bearer token"} + await transport._initialize_session(headers=test_headers) + + assert transport._send_request.call_count == 2 + + init_call = transport._send_request.call_args_list[0] + assert isinstance(init_call.kwargs["request"], types.InitializeRequest) + assert init_call.kwargs["headers"] == test_headers + + notify_call = transport._send_request.call_args_list[1] + assert isinstance( + notify_call.kwargs["request"], types.InitializedNotification + ) + assert notify_call.kwargs["headers"] == test_headers + # --- Tool Management Tests --- async def test_tools_list_success(self, transport, mocker): From f0b4d90711aade07cbd872e692118102b69ed241 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 Jan 2026 13:33:51 +0530 Subject: [PATCH 02/18] feat(mcp): add new MCP version --- packages/toolbox-core/README.md | 3 +- .../toolbox-core/src/toolbox_core/client.py | 4 +- .../toolbox_core/mcp_transport/__init__.py | 2 + .../mcp_transport/v20251125/mcp.py | 191 ++++++++++++ .../mcp_transport/v20251125/types.py | 160 ++++++++++ .../toolbox-core/src/toolbox_core/protocol.py | 4 +- .../tests/mcp_transport/test_v20251125.py | 275 ++++++++++++++++++ packages/toolbox-core/tests/test_e2e_mcp.py | 6 +- packages/toolbox-langchain/README.md | 3 +- .../tests/test_async_client.py | 2 +- .../toolbox-langchain/tests/test_client.py | 2 +- packages/toolbox-llamaindex/README.md | 3 +- .../tests/test_async_client.py | 2 +- .../toolbox-llamaindex/tests/test_client.py | 2 +- 14 files changed, 645 insertions(+), 14 deletions(-) create mode 100644 packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/mcp.py create mode 100644 packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/types.py create mode 100644 packages/toolbox-core/tests/mcp_transport/test_v20251125.py diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index d9933866..7e7857c8 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -159,8 +159,9 @@ You can explicitly select a protocol using the `protocol` option during client i | Constant | Description | | :--- | :--- | -| `Protocol.MCP` | **(Default)** Alias for the latest supported MCP version (currently `v2025-06-18`). | +| `Protocol.MCP` | **(Default)** Alias for the latest supported MCP version (currently `v2025-11-25`). | | `Protocol.TOOLBOX` | The native Toolbox HTTP protocol. | +| `Protocol.MCP_v20251125` | MCP Protocol version 2025-11-25. | | `Protocol.MCP_v20250618` | MCP Protocol version 2025-06-18. | | `Protocol.MCP_v20250326` | MCP Protocol version 2025-03-26. | | `Protocol.MCP_v20241105` | MCP Protocol version 2024-11-05. | diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index 2a1d27c4..9f216e07 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -67,7 +67,9 @@ def __init__( if protocol == Protocol.TOOLBOX: self.__transport = ToolboxTransport(url, session) elif protocol in Protocol.get_supported_mcp_versions(): - if protocol == Protocol.MCP_v20250618: + if protocol == Protocol.MCP_v20251125: + self.__transport = McpHttpTransportV20251125(url, session, protocol) + elif protocol == Protocol.MCP_v20250618: self.__transport = McpHttpTransportV20250618(url, session, protocol) elif protocol == Protocol.MCP_v20250326: self.__transport = McpHttpTransportV20250326(url, session, protocol) diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/__init__.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/__init__.py index 8813dc52..95a93a79 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/__init__.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/__init__.py @@ -15,9 +15,11 @@ from .v20241105.mcp import McpHttpTransportV20241105 from .v20250326.mcp import McpHttpTransportV20250326 from .v20250618.mcp import McpHttpTransportV20250618 +from .v20251125.mcp import McpHttpTransportV20251125 __all__ = [ "McpHttpTransportV20241105", "McpHttpTransportV20250326", "McpHttpTransportV20250618", + "McpHttpTransportV20251125", ] 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 new file mode 100644 index 00000000..f7ce9958 --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/mcp.py @@ -0,0 +1,191 @@ +# 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. + +from typing import Mapping, Optional, TypeVar + +from pydantic import BaseModel + +from ... import version +from ...protocol import ManifestSchema +from ..transport_base import _McpHttpTransportBase +from . import types + +ReceiveResultT = TypeVar("ReceiveResultT", bound=BaseModel) + + +class McpHttpTransportV20251125(_McpHttpTransportBase): + """Transport for the MCP v2025-11-25 protocol.""" + + async def _send_request( + self, + url: str, + request: types.MCPRequest[ReceiveResultT] | types.MCPNotification, + headers: Optional[Mapping[str, str]] = None, + ) -> ReceiveResultT | None: + """Sends a JSON-RPC request to the MCP server.""" + req_headers = dict(headers or {}) + req_headers["MCP-Protocol-Version"] = self._protocol_version + + params = ( + request.params.model_dump(mode="json", exclude_none=True) + if isinstance(request.params, BaseModel) + else request.params + ) + + rpc_msg: BaseModel + if isinstance(request, types.MCPNotification): + rpc_msg = types.JSONRPCNotification(method=request.method, params=params) + else: + rpc_msg = types.JSONRPCRequest(method=request.method, params=params) + + payload = rpc_msg.model_dump(mode="json", exclude_none=True) + + async with self._session.post( + url, json=payload, headers=req_headers + ) as response: + if not response.ok: + error_text = await response.text() + raise RuntimeError( + "API request failed with status" + f" {response.status} ({response.reason}). Server response:" + f" {error_text}" + ) + + if response.status == 204 or response.content.at_eof(): + return None + + json_resp = await response.json() + + # Check for JSON-RPC Error + if "error" in json_resp: + try: + err = types.JSONRPCError.model_validate(json_resp).error + raise RuntimeError( + f"MCP request failed with code {err.code}: {err.message}" + ) + except Exception: + # Fallback if the error doesn't match our schema exactly + raw_error = json_resp.get("error", {}) + raise RuntimeError(f"MCP request failed: {raw_error}") + + # Parse Result + if isinstance(request, types.MCPRequest): + try: + rpc_resp = types.JSONRPCResponse.model_validate(json_resp) + return request.get_result_model().model_validate(rpc_resp.result) + except Exception as e: + raise RuntimeError(f"Failed to parse JSON-RPC response: {e}") + return None + + async def _initialize_session( + self, headers: Optional[Mapping[str, str]] = None + ) -> None: + """Initializes the MCP session.""" + params = types.InitializeRequestParams( + protocolVersion=self._protocol_version, + capabilities=types.ClientCapabilities(), + clientInfo=types.Implementation( + name="toolbox-python-sdk", version=version.__version__ + ), + ) + + result = await self._send_request( + url=self._mcp_base_url, + request=types.InitializeRequest(params=params), + headers=headers, + ) + + if result is None: + raise RuntimeError("Failed to initialize session: No response from server.") + + self._server_version = result.serverInfo.version + + if result.protocolVersion != self._protocol_version: + raise RuntimeError( + "MCP version mismatch: client does not support server version" + f" {result.protocolVersion}" + ) + + if not result.capabilities.tools: + if self._manage_session: + await self.close() + raise RuntimeError("Server does not support the 'tools' capability.") + + await self._send_request( + url=self._mcp_base_url, + request=types.InitializedNotification(), + headers=headers, + ) + + async def tools_list( + self, + toolset_name: Optional[str] = None, + headers: Optional[Mapping[str, str]] = None, + ) -> ManifestSchema: + """Lists available tools from the server using the MCP protocol.""" + await self._ensure_initialized(headers=headers) + + url = self._mcp_base_url + (toolset_name if toolset_name else "") + result = await self._send_request( + url=url, request=types.ListToolsRequest(), headers=headers + ) + if result is None: + raise RuntimeError("Failed to list tools: No response from server.") + + tools_map = { + t.name: self._convert_tool_schema(t.model_dump(mode="json", by_alias=True)) + for t in result.tools + } + if self._server_version is None: + raise RuntimeError("Server version not available.") + + return ManifestSchema( + serverVersion=self._server_version, + tools=tools_map, + ) + + async def tool_get( + self, tool_name: str, headers: Optional[Mapping[str, str]] = None + ) -> ManifestSchema: + """Gets a single tool from the server by listing all and filtering.""" + manifest = await self.tools_list(headers=headers) + + if tool_name not in manifest.tools: + raise ValueError(f"Tool '{tool_name}' not found.") + + return ManifestSchema( + serverVersion=manifest.serverVersion, + tools={tool_name: manifest.tools[tool_name]}, + ) + + async def tool_invoke( + self, tool_name: str, arguments: dict, headers: Optional[Mapping[str, str]] + ) -> str: + """Invokes a specific tool on the server using the MCP protocol.""" + await self._ensure_initialized(headers=headers) + + result = await self._send_request( + url=self._mcp_base_url, + request=types.CallToolRequest( + params=types.CallToolRequestParams(name=tool_name, arguments=arguments) + ), + headers=headers, + ) + + if result is None: + raise RuntimeError( + f"Failed to invoke tool '{tool_name}': No response from server." + ) + + return "".join(c.text for c in result.content if c.type == "text") or "null" diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/types.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/types.py new file mode 100644 index 00000000..4cbcfa99 --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/types.py @@ -0,0 +1,160 @@ +# 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. + +import uuid +from typing import Any, Generic, Literal, Type, TypeVar + +from pydantic import BaseModel, ConfigDict, Field + + +class _BaseMCPModel(BaseModel): + """Base model with common configuration.""" + + model_config = ConfigDict(extra="allow") + + +class RequestParams(_BaseMCPModel): + pass + + +class JSONRPCRequest(_BaseMCPModel): + jsonrpc: Literal["2.0"] = "2.0" + id: str | int = Field(default_factory=lambda: str(uuid.uuid4())) + method: str + params: dict[str, Any] | None = None + + +class JSONRPCNotification(_BaseMCPModel): + """A notification which does not expect a response (no ID).""" + + jsonrpc: Literal["2.0"] = "2.0" + method: str + params: dict[str, Any] | None = None + + +class JSONRPCResponse(_BaseMCPModel): + jsonrpc: Literal["2.0"] + id: str | int + result: dict[str, Any] + + +class ErrorData(_BaseMCPModel): + code: int + message: str + data: Any | None = None + + +class JSONRPCError(_BaseMCPModel): + jsonrpc: Literal["2.0"] + id: str | int + error: ErrorData + + +class BaseMetadata(_BaseMCPModel): + name: str + + +class Implementation(BaseMetadata): + version: str + + +class ClientCapabilities(_BaseMCPModel): + pass + + +class InitializeRequestParams(RequestParams): + protocolVersion: str + capabilities: ClientCapabilities + clientInfo: Implementation + + +class ServerCapabilities(_BaseMCPModel): + prompts: dict[str, Any] | None = None + tools: dict[str, Any] | None = None + + +class InitializeResult(_BaseMCPModel): + protocolVersion: str + capabilities: ServerCapabilities + serverInfo: Implementation + instructions: str | None = None + + +class Tool(BaseMetadata): + description: str | None = None + inputSchema: dict[str, Any] + + +class ListToolsResult(_BaseMCPModel): + tools: list[Tool] + + +class TextContent(_BaseMCPModel): + type: Literal["text"] + text: str + + +class CallToolResult(_BaseMCPModel): + content: list[TextContent] + isError: bool = False + + +ResultT = TypeVar("ResultT", bound=BaseModel) + + +class MCPRequest(_BaseMCPModel, Generic[ResultT]): + method: str + params: dict[str, Any] | BaseModel | None = None + + def get_result_model(self) -> Type[ResultT]: + raise NotImplementedError + + +class MCPNotification(_BaseMCPModel): + method: str + params: dict[str, Any] | BaseModel | None = None + + +class InitializeRequest(MCPRequest[InitializeResult]): + method: Literal["initialize"] = "initialize" + params: InitializeRequestParams + + def get_result_model(self) -> Type[InitializeResult]: + return InitializeResult + + +class InitializedNotification(MCPNotification): + method: Literal["notifications/initialized"] = "notifications/initialized" + params: dict[str, Any] = {} + + +class ListToolsRequest(MCPRequest[ListToolsResult]): + method: Literal["tools/list"] = "tools/list" + params: dict[str, Any] = {} + + def get_result_model(self) -> Type[ListToolsResult]: + return ListToolsResult + + +class CallToolRequestParams(_BaseMCPModel): + name: str + arguments: dict[str, Any] + + +class CallToolRequest(MCPRequest[CallToolResult]): + method: Literal["tools/call"] = "tools/call" + params: CallToolRequestParams + + def get_result_model(self) -> Type[CallToolResult]: + return CallToolResult diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index c58caf1d..97e1005c 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -25,12 +25,14 @@ class Protocol(str, Enum): MCP_v20250618 = "2025-06-18" MCP_v20250326 = "2025-03-26" MCP_v20241105 = "2024-11-05" - MCP = MCP_v20250618 + MCP_v20251125 = "2025-11-25" + MCP = MCP_v20251125 @staticmethod def get_supported_mcp_versions() -> list[str]: """Returns a list of supported MCP protocol versions.""" return [ + Protocol.MCP_v20251125.value, Protocol.MCP_v20250618.value, Protocol.MCP_v20250326.value, Protocol.MCP_v20241105.value, diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py new file mode 100644 index 00000000..f1ff824c --- /dev/null +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -0,0 +1,275 @@ +# Copyright 2025 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. + +from unittest.mock import AsyncMock, Mock, patch + +import pytest +import pytest_asyncio +from aiohttp import ClientSession + +from toolbox_core.mcp_transport.v20251125 import types +from toolbox_core.mcp_transport.v20251125.mcp import McpHttpTransportv20251125 +from toolbox_core.protocol import ManifestSchema, Protocol + + +def create_fake_tools_list_result(): + return types.ListToolsResult( + tools=[ + types.Tool( + name="get_weather", + description="Gets the weather.", + inputSchema={ + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, + ) + ] + ) + + +@pytest_asyncio.fixture +async def transport(): + mock_session = AsyncMock(spec=ClientSession) + transport = McpHttpTransportv20251125( + "http://fake-server.com", session=mock_session, protocol=Protocol.MCP_v20251125 + ) + yield transport + await transport.close() + + +@pytest.mark.asyncio +class TestMcpHttpTransportv20251125: + + # --- Request Sending Tests (Standard + Header) --- + + async def test_send_request_success(self, transport): + mock_response = AsyncMock() + mock_response.ok = True + mock_response.status = 200 + mock_response.content = Mock() + mock_response.content.at_eof.return_value = False + mock_response.json.return_value = {"jsonrpc": "2.0", "id": "1", "result": {}} + transport._session.post.return_value.__aenter__.return_value = mock_response + + class TestResult(types.BaseModel): + pass + + class TestRequest(types.MCPRequest[TestResult]): + method: str = "method" + params: dict = {} + + def get_result_model(self): + return TestResult + + result = await transport._send_request("url", TestRequest()) + assert result == TestResult() + + async def test_send_request_adds_protocol_header(self, transport): + """Test that the MCP-Protocol-Version header is added.""" + mock_response = AsyncMock() + mock_response.ok = True + mock_response.content = Mock() + mock_response.content.at_eof.return_value = False + mock_response.json.return_value = {"jsonrpc": "2.0", "id": "1", "result": {}} + transport._session.post.return_value.__aenter__.return_value = mock_response + + class TestResult(types.BaseModel): + pass + + class TestRequest(types.MCPRequest[TestResult]): + method: str = "method" + params: dict = {} + + def get_result_model(self): + return TestResult + + await transport._send_request("url", TestRequest()) + + call_args = transport._session.post.call_args + headers = call_args.kwargs["headers"] + assert headers["MCP-Protocol-Version"] == "2025-11-25" + + async def test_send_request_api_error(self, transport): + mock_response = AsyncMock() + mock_response.ok = False + mock_response.status = 500 + mock_response.text.return_value = "Error" + transport._session.post.return_value.__aenter__.return_value = mock_response + + class TestResult(types.BaseModel): + pass + + class TestRequest(types.MCPRequest[TestResult]): + method: str = "method" + params: dict = {} + + def get_result_model(self): + return TestResult + + with pytest.raises(RuntimeError, match="API request failed"): + await transport._send_request("url", TestRequest()) + + async def test_send_request_mcp_error(self, transport): + mock_response = AsyncMock() + mock_response.ok = True + mock_response.status = 200 + mock_response.content = Mock() + mock_response.content.at_eof.return_value = False + mock_response.json.return_value = { + "jsonrpc": "2.0", + "id": "1", + "error": {"code": -32601, "message": "Error"}, + } + transport._session.post.return_value.__aenter__.return_value = mock_response + + class TestResult(types.BaseModel): + pass + + class TestRequest(types.MCPRequest[TestResult]): + method: str = "method" + params: dict = {} + + def get_result_model(self): + return TestResult + + with pytest.raises(RuntimeError, match="MCP request failed"): + await transport._send_request("url", TestRequest()) + + async def test_send_notification(self, transport): + mock_response = AsyncMock() + mock_response.ok = True + mock_response.status = 204 + transport._session.post.return_value.__aenter__.return_value = mock_response + + class TestNotification(types.MCPNotification): + method: str = "notifications/test" + params: dict = {} + + await transport._send_request("url", TestNotification()) + payload = transport._session.post.call_args.kwargs["json"] + assert "id" not in payload + + # --- Initialization Tests --- + + @patch("toolbox_core.mcp_transport.v20251125.mcp.version") + async def test_initialize_session_success(self, mock_version, transport, mocker): + mock_version.__version__ = "1.2.3" + mock_send = mocker.patch.object( + transport, "_send_request", new_callable=AsyncMock + ) + + mock_send.side_effect = [ + types.InitializeResult( + protocolVersion="2025-11-25", + capabilities=types.ServerCapabilities(tools={"listChanged": True}), + serverInfo=types.Implementation(name="test", version="1.0"), + ), + None, + ] + + await transport._initialize_session() + assert transport._server_version == "1.0" + + async def test_initialize_session_protocol_mismatch(self, transport, mocker): + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=types.InitializeResult( + protocolVersion="2099-01-01", + capabilities=types.ServerCapabilities(tools={"listChanged": True}), + serverInfo=types.Implementation(name="test", version="1.0"), + ), + ) + + with pytest.raises(RuntimeError, match="MCP version mismatch"): + await transport._initialize_session() + + async def test_initialize_session_missing_tools_capability(self, transport, mocker): + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=types.InitializeResult( + protocolVersion="2025-11-25", + capabilities=types.ServerCapabilities(), + serverInfo=types.Implementation(name="test", version="1.0"), + ), + ) + + with pytest.raises( + RuntimeError, match="Server does not support the 'tools' capability" + ): + await transport._initialize_session() + + # --- Tool Management Tests --- + + async def test_tools_list_success(self, transport, mocker): + mocker.patch.object(transport, "_ensure_initialized", new_callable=AsyncMock) + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=create_fake_tools_list_result(), + ) + transport._server_version = "1.0" + manifest = await transport.tools_list() + assert isinstance(manifest, ManifestSchema) + + async def test_tools_list_with_toolset_name(self, transport, mocker): + """Test listing tools with a specific toolset name updates the URL.""" + mocker.patch.object(transport, "_ensure_initialized", new_callable=AsyncMock) + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=create_fake_tools_list_result(), + ) + transport._server_version = "1.0.0" + + manifest = await transport.tools_list(toolset_name="custom_toolset") + + assert isinstance(manifest, ManifestSchema) + expected_url = transport.base_url + "custom_toolset" + + call_args = transport._send_request.call_args + assert call_args.kwargs["url"] == expected_url + assert isinstance(call_args.kwargs["request"], types.ListToolsRequest) + assert call_args.kwargs["headers"] is None + + async def test_tool_invoke_success(self, transport, mocker): + mocker.patch.object(transport, "_ensure_initialized", new_callable=AsyncMock) + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=types.CallToolResult( + content=[types.TextContent(type="text", text="Result")] + ), + ) + result = await transport.tool_invoke("tool", {}, {}) + assert result == "Result" + + async def test_tool_get_success(self, transport, mocker): + mocker.patch.object(transport, "_ensure_initialized", new_callable=AsyncMock) + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=create_fake_tools_list_result(), + ) + transport._server_version = "1.0" + manifest = await transport.tool_get("get_weather") + assert "get_weather" in manifest.tools diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index 37f6c751..c644136f 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -26,11 +26,7 @@ @pytest_asyncio.fixture( scope="function", - params=[ - Protocol.MCP_v20250618, - Protocol.MCP_v20250326, - Protocol.MCP_v20241105, - ], + params=Protocol.get_supported_mcp_versions(), ) async def toolbox(request): """Creates a ToolboxClient instance shared by all tests in this module.""" diff --git a/packages/toolbox-langchain/README.md b/packages/toolbox-langchain/README.md index cd25bca1..b43f3cb1 100644 --- a/packages/toolbox-langchain/README.md +++ b/packages/toolbox-langchain/README.md @@ -106,8 +106,9 @@ You can explicitly select a protocol using the `protocol` option during client i | Constant | Description | | :--- | :--- | -| `Protocol.MCP` | **(Default)** Alias for the latest supported MCP version (currently `v2025-06-18`). | +| `Protocol.MCP` | **(Default)** Alias for the latest supported MCP version (currently `v2025-11-25`). | | `Protocol.TOOLBOX` | The native Toolbox HTTP protocol. | +| `Protocol.MCP_v20251125` | MCP Protocol version 2025-11-25. | | `Protocol.MCP_v20250618` | MCP Protocol version 2025-06-18. | | `Protocol.MCP_v20250326` | MCP Protocol version 2025-03-26. | | `Protocol.MCP_v20241105` | MCP Protocol version 2024-11-05. | diff --git a/packages/toolbox-langchain/tests/test_async_client.py b/packages/toolbox-langchain/tests/test_async_client.py index ced88bba..be895554 100644 --- a/packages/toolbox-langchain/tests/test_async_client.py +++ b/packages/toolbox-langchain/tests/test_async_client.py @@ -352,5 +352,5 @@ async def test_init_with_client_headers( url=URL, session=mock_session, client_headers=headers, - protocol=Protocol.MCP_v20250618, + protocol=Protocol.MCP_v20251125, ) diff --git a/packages/toolbox-langchain/tests/test_client.py b/packages/toolbox-langchain/tests/test_client.py index 4ec8d52e..344f2a37 100644 --- a/packages/toolbox-langchain/tests/test_client.py +++ b/packages/toolbox-langchain/tests/test_client.py @@ -491,7 +491,7 @@ def test_init_with_client_headers(self, mock_core_client_constructor): headers = {"X-Test-Header": "value"} ToolboxClient(URL, client_headers=headers) mock_core_client_constructor.assert_called_once_with( - url=URL, client_headers=headers, protocol=Protocol.MCP_v20250618 + url=URL, client_headers=headers, protocol=Protocol.MCP_v20251125 ) @patch("toolbox_langchain.client.ToolboxCoreSyncClient") diff --git a/packages/toolbox-llamaindex/README.md b/packages/toolbox-llamaindex/README.md index f3b9615c..3bac09ab 100644 --- a/packages/toolbox-llamaindex/README.md +++ b/packages/toolbox-llamaindex/README.md @@ -110,8 +110,9 @@ You can explicitly select a protocol using the `protocol` option during client i | Constant | Description | | :--- | :--- | -| `Protocol.MCP` | **(Default)** Alias for the latest supported MCP version (currently `v2025-06-18`). | +| `Protocol.MCP` | **(Default)** Alias for the latest supported MCP version (currently `v2025-11-25`). | | `Protocol.TOOLBOX` | The native Toolbox HTTP protocol. | +| `Protocol.MCP_v20251125` | MCP Protocol version 2025-11-25. | | `Protocol.MCP_v20250618` | MCP Protocol version 2025-06-18. | | `Protocol.MCP_v20250326` | MCP Protocol version 2025-03-26. | | `Protocol.MCP_v20241105` | MCP Protocol version 2024-11-05. | diff --git a/packages/toolbox-llamaindex/tests/test_async_client.py b/packages/toolbox-llamaindex/tests/test_async_client.py index 1f295172..18d1e296 100644 --- a/packages/toolbox-llamaindex/tests/test_async_client.py +++ b/packages/toolbox-llamaindex/tests/test_async_client.py @@ -352,5 +352,5 @@ async def test_init_with_client_headers( url=URL, session=mock_session, client_headers=headers, - protocol=Protocol.MCP_v20250618, + protocol=Protocol.MCP_v20251125, ) diff --git a/packages/toolbox-llamaindex/tests/test_client.py b/packages/toolbox-llamaindex/tests/test_client.py index 8af39123..78e701f5 100644 --- a/packages/toolbox-llamaindex/tests/test_client.py +++ b/packages/toolbox-llamaindex/tests/test_client.py @@ -430,7 +430,7 @@ def test_init_with_client_headers(self, mock_core_client_constructor): headers = {"X-Test-Header": "value"} ToolboxClient(URL, client_headers=headers) mock_core_client_constructor.assert_called_once_with( - url=URL, client_headers=headers, protocol=Protocol.MCP_v20250618 + url=URL, client_headers=headers, protocol=Protocol.MCP_v20251125 ) @patch("toolbox_llamaindex.client.ToolboxCoreSyncClient") From 478821b3fdac7585c94d5cd03d17e95d7d08f2fd Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:36:49 +0530 Subject: [PATCH 03/18] fix license header --- packages/toolbox-core/tests/mcp_transport/test_v20251125.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py index f1ff824c..ead100d8 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google LLC +# 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. From 8919cd34636ad6447e016d87458dcf62988ebc80 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 Jan 2026 13:39:36 +0530 Subject: [PATCH 04/18] fix tests --- packages/toolbox-core/tests/mcp_transport/test_v20251125.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py index ead100d8..90962604 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -19,7 +19,7 @@ from aiohttp import ClientSession from toolbox_core.mcp_transport.v20251125 import types -from toolbox_core.mcp_transport.v20251125.mcp import McpHttpTransportv20251125 +from toolbox_core.mcp_transport.v20251125.mcp import McpHttpTransportV20251125 from toolbox_core.protocol import ManifestSchema, Protocol @@ -42,7 +42,7 @@ def create_fake_tools_list_result(): @pytest_asyncio.fixture async def transport(): mock_session = AsyncMock(spec=ClientSession) - transport = McpHttpTransportv20251125( + transport = McpHttpTransportV20251125( "http://fake-server.com", session=mock_session, protocol=Protocol.MCP_v20251125 ) yield transport From 66222dbe2793f6b536c71ba92ccd0e0c4785047f Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 Jan 2026 13:40:06 +0530 Subject: [PATCH 05/18] fix tests --- packages/toolbox-core/tests/mcp_transport/test_v20251125.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py index 90962604..340e2355 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -50,7 +50,7 @@ async def transport(): @pytest.mark.asyncio -class TestMcpHttpTransportv20251125: +class TestMcpHttpTransportV20251125: # --- Request Sending Tests (Standard + Header) --- From 3a4da2f22d138fd9ebf3624e1f63df47f1c1a8e0 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 Jan 2026 13:41:14 +0530 Subject: [PATCH 06/18] fix imports --- packages/toolbox-core/src/toolbox_core/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index 9f216e07..3e69afea 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -24,6 +24,7 @@ McpHttpTransportV20241105, McpHttpTransportV20250326, McpHttpTransportV20250618, + McpHttpTransportV20251125, ) from .protocol import Protocol, ToolSchema from .tool import ToolboxTool From f45cb006f97b88a75fb39292e6e61574f9c02645 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 Jan 2026 13:48:15 +0530 Subject: [PATCH 07/18] fix tests --- packages/toolbox-core/tests/test_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_protocol.py b/packages/toolbox-core/tests/test_protocol.py index b5f00067..8dd60e3f 100644 --- a/packages/toolbox-core/tests/test_protocol.py +++ b/packages/toolbox-core/tests/test_protocol.py @@ -26,7 +26,7 @@ def test_get_supported_mcp_versions(): Tests that get_supported_mcp_versions returns the correct list of versions, sorted from newest to oldest. """ - expected_versions = ["2025-06-18", "2025-03-26", "2024-11-05"] + expected_versions = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"] supported_versions = Protocol.get_supported_mcp_versions() assert supported_versions == expected_versions From b584bdddd26e47f0df04e6f84e81caad33ffe73e Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 Jan 2026 13:52:39 +0530 Subject: [PATCH 08/18] update toolbox version --- .../toolbox-adk/integration.cloudbuild.yaml | 2 +- .../toolbox-core/integration.cloudbuild.yaml | 24 +++++++++---------- .../integration.cloudbuild.yaml | 24 +++++++++---------- .../integration.cloudbuild.yaml | 24 +++++++++---------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/toolbox-adk/integration.cloudbuild.yaml b/packages/toolbox-adk/integration.cloudbuild.yaml index e5ba0ab4..ba926b89 100644 --- a/packages/toolbox-adk/integration.cloudbuild.yaml +++ b/packages/toolbox-adk/integration.cloudbuild.yaml @@ -55,5 +55,5 @@ options: substitutions: _VERSION: '3.13' # Default values (can be overridden by triggers) - _TOOLBOX_VERSION: '0.22.0' + _TOOLBOX_VERSION: '1.0.0' _TOOLBOX_MANIFEST_VERSION: '34' diff --git a/packages/toolbox-core/integration.cloudbuild.yaml b/packages/toolbox-core/integration.cloudbuild.yaml index 6c346f83..1fc30020 100644 --- a/packages/toolbox-core/integration.cloudbuild.yaml +++ b/packages/toolbox-core/integration.cloudbuild.yaml @@ -14,11 +14,11 @@ steps: - id: Install library requirements - name: 'python:${_VERSION}' - dir: 'packages/toolbox-core' + name: "python:${_VERSION}" + dir: "packages/toolbox-core" env: args: - - '-c' + - "-c" - | pip install uv uv venv /workspace/venv @@ -27,25 +27,25 @@ steps: uv pip install -r requirements.txt entrypoint: /bin/bash - id: Install test requirements - name: 'python:${_VERSION}' - dir: 'packages/toolbox-core' + name: "python:${_VERSION}" + dir: "packages/toolbox-core" env: args: - - '-c' + - "-c" - | source /workspace/venv/bin/activate uv pip install -e '.[test]' entrypoint: /bin/bash - id: Run integration tests - name: 'python:${_VERSION}' - dir: 'packages/toolbox-core' + name: "python:${_VERSION}" + dir: "packages/toolbox-core" env: - TOOLBOX_URL=$_TOOLBOX_URL - TOOLBOX_VERSION=$_TOOLBOX_VERSION - GOOGLE_CLOUD_PROJECT=$PROJECT_ID - TOOLBOX_MANIFEST_VERSION=${_TOOLBOX_MANIFEST_VERSION} args: - - '-c' + - "-c" - | source /workspace/venv/bin/activate python -m pytest --cov=src/toolbox_core --cov-report=term --cov-fail-under=90 tests/ @@ -53,6 +53,6 @@ steps: options: logging: CLOUD_LOGGING_ONLY substitutions: - _VERSION: '3.13' - _TOOLBOX_VERSION: '0.26.0' - _TOOLBOX_MANIFEST_VERSION: '34' + _VERSION: "3.13" + _TOOLBOX_VERSION: "0.26.0" + _TOOLBOX_MANIFEST_VERSION: "34" diff --git a/packages/toolbox-langchain/integration.cloudbuild.yaml b/packages/toolbox-langchain/integration.cloudbuild.yaml index adc59d87..74f0e582 100644 --- a/packages/toolbox-langchain/integration.cloudbuild.yaml +++ b/packages/toolbox-langchain/integration.cloudbuild.yaml @@ -14,11 +14,11 @@ steps: - id: Install library requirements - name: 'python:${_VERSION}' - dir: 'packages/toolbox-langchain' + name: "python:${_VERSION}" + dir: "packages/toolbox-langchain" env: args: - - '-c' + - "-c" - | pip install uv uv venv /workspace/venv @@ -27,25 +27,25 @@ steps: uv pip install -r requirements.txt entrypoint: /bin/bash - id: Install test requirements - name: 'python:${_VERSION}' - dir: 'packages/toolbox-langchain' + name: "python:${_VERSION}" + dir: "packages/toolbox-langchain" env: args: - - '-c' + - "-c" - | source /workspace/venv/bin/activate uv pip install -e '.[test]' entrypoint: /bin/bash - id: Run integration tests - name: 'python:${_VERSION}' - dir: 'packages/toolbox-langchain' + name: "python:${_VERSION}" + dir: "packages/toolbox-langchain" env: - TOOLBOX_URL=$_TOOLBOX_URL - TOOLBOX_VERSION=$_TOOLBOX_VERSION - GOOGLE_CLOUD_PROJECT=$PROJECT_ID - TOOLBOX_MANIFEST_VERSION=${_TOOLBOX_MANIFEST_VERSION} args: - - '-c' + - "-c" - | source /workspace/venv/bin/activate python -m pytest --cov=src/toolbox_langchain --cov-report=term --cov-fail-under=90 tests/ @@ -53,6 +53,6 @@ steps: options: logging: CLOUD_LOGGING_ONLY substitutions: - _VERSION: '3.13' - _TOOLBOX_VERSION: '0.26.0' - _TOOLBOX_MANIFEST_VERSION: '34' + _VERSION: "3.13" + _TOOLBOX_VERSION: "0.26.0" + _TOOLBOX_MANIFEST_VERSION: "34" diff --git a/packages/toolbox-llamaindex/integration.cloudbuild.yaml b/packages/toolbox-llamaindex/integration.cloudbuild.yaml index e723a655..3549aae3 100644 --- a/packages/toolbox-llamaindex/integration.cloudbuild.yaml +++ b/packages/toolbox-llamaindex/integration.cloudbuild.yaml @@ -14,11 +14,11 @@ steps: - id: Install library requirements - name: 'python:${_VERSION}' - dir: 'packages/toolbox-llamaindex' + name: "python:${_VERSION}" + dir: "packages/toolbox-llamaindex" env: args: - - '-c' + - "-c" - | pip install uv uv venv /workspace/venv @@ -27,25 +27,25 @@ steps: uv pip install -r requirements.txt entrypoint: /bin/bash - id: Install test requirements - name: 'python:${_VERSION}' - dir: 'packages/toolbox-llamaindex' + name: "python:${_VERSION}" + dir: "packages/toolbox-llamaindex" env: args: - - '-c' + - "-c" - | source /workspace/venv/bin/activate uv pip install -e '.[test]' entrypoint: /bin/bash - id: Run integration tests - name: 'python:${_VERSION}' - dir: 'packages/toolbox-llamaindex' + name: "python:${_VERSION}" + dir: "packages/toolbox-llamaindex" env: - TOOLBOX_URL=$_TOOLBOX_URL - TOOLBOX_VERSION=$_TOOLBOX_VERSION - GOOGLE_CLOUD_PROJECT=$PROJECT_ID - TOOLBOX_MANIFEST_VERSION=${_TOOLBOX_MANIFEST_VERSION} args: - - '-c' + - "-c" - | source /workspace/venv/bin/activate python -m pytest --cov=src/toolbox_llamaindex --cov-report=term --cov-fail-under=90 tests/ @@ -53,6 +53,6 @@ steps: options: logging: CLOUD_LOGGING_ONLY substitutions: - _VERSION: '3.13' - _TOOLBOX_VERSION: '0.26.0' - _TOOLBOX_MANIFEST_VERSION: '34' + _VERSION: "3.13" + _TOOLBOX_VERSION: "0.26.0" + _TOOLBOX_MANIFEST_VERSION: "34" From 6566d92936f2e64970a91adba5f003a5b1ba02ce Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 Jan 2026 14:00:57 +0530 Subject: [PATCH 09/18] use toolbox dev version. Revert this commit before PR push --- packages/toolbox-core/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/tests/conftest.py b/packages/toolbox-core/tests/conftest.py index 633d765e..e958bc7d 100644 --- a/packages/toolbox-core/tests/conftest.py +++ b/packages/toolbox-core/tests/conftest.py @@ -75,7 +75,7 @@ def get_toolbox_binary_url(toolbox_version: str) -> str: arch = ( "arm64" if os_system == "darwin" and platform.machine() == "arm64" else "amd64" ) - return f"v{toolbox_version}/{os_system}/{arch}/toolbox" + return f"continuous-release/{os_system}/{arch}/toolbox" def get_auth_token(client_id: str) -> str: @@ -136,7 +136,7 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None """Starts the toolbox server as a subprocess.""" print("Downloading toolbox binary from gcs bucket...") source_blob_name = get_toolbox_binary_url(toolbox_version) - download_blob("genai-toolbox", source_blob_name, "toolbox") + download_blob("genai-toolbox-dev", source_blob_name, "toolbox") print("Toolbox binary downloaded successfully.") try: print("Opening toolbox server process...") From 90c9cb7c76a40672797b8ae00c39cbde6b8599ca Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 Jan 2026 14:20:46 +0530 Subject: [PATCH 10/18] Revert "use toolbox dev version. Revert this commit before PR push" This reverts commit be6af419c055432fc010bdf931c2086a5e9540d9. --- packages/toolbox-core/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/tests/conftest.py b/packages/toolbox-core/tests/conftest.py index e958bc7d..633d765e 100644 --- a/packages/toolbox-core/tests/conftest.py +++ b/packages/toolbox-core/tests/conftest.py @@ -75,7 +75,7 @@ def get_toolbox_binary_url(toolbox_version: str) -> str: arch = ( "arm64" if os_system == "darwin" and platform.machine() == "arm64" else "amd64" ) - return f"continuous-release/{os_system}/{arch}/toolbox" + return f"v{toolbox_version}/{os_system}/{arch}/toolbox" def get_auth_token(client_id: str) -> str: @@ -136,7 +136,7 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None """Starts the toolbox server as a subprocess.""" print("Downloading toolbox binary from gcs bucket...") source_blob_name = get_toolbox_binary_url(toolbox_version) - download_blob("genai-toolbox-dev", source_blob_name, "toolbox") + download_blob("genai-toolbox", source_blob_name, "toolbox") print("Toolbox binary downloaded successfully.") try: print("Opening toolbox server process...") From d319855a30fd925ba7bd9a072fd4016c29a2b91f Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:47:22 +0530 Subject: [PATCH 11/18] Update integration.cloudbuild.yaml --- packages/toolbox-adk/integration.cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-adk/integration.cloudbuild.yaml b/packages/toolbox-adk/integration.cloudbuild.yaml index ba926b89..11d99cfc 100644 --- a/packages/toolbox-adk/integration.cloudbuild.yaml +++ b/packages/toolbox-adk/integration.cloudbuild.yaml @@ -55,5 +55,5 @@ options: substitutions: _VERSION: '3.13' # Default values (can be overridden by triggers) - _TOOLBOX_VERSION: '1.0.0' + _TOOLBOX_VERSION: '0.26.0' _TOOLBOX_MANIFEST_VERSION: '34' From afd7b1f71334afa481942e39479daa9a585583af Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 27 Jan 2026 12:26:56 +0530 Subject: [PATCH 12/18] fix tests --- .../tests/mcp_transport/test_v20251125.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py index 340e2355..bfde63bc 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -214,6 +214,38 @@ async def test_initialize_session_missing_tools_capability(self, transport, mock ): await transport._initialize_session() + async def test_ensure_initialized_passes_headers(self, transport): + transport._initialize_session = AsyncMock() + + test_headers = {"X-Test": "123"} + await transport._ensure_initialized(headers=test_headers) + + transport._initialize_session.assert_called_with(headers=test_headers) + + async def test_initialize_passes_headers_to_request(self, transport): + transport._send_request = AsyncMock() + transport._send_request.return_value = types.InitializeResult( + protocolVersion="2025-11-25", + capabilities=types.ServerCapabilities(tools={"listChanged": True}), + serverInfo=types.Implementation(name="test", version="1.0"), + ) + + test_headers = {"Authorization": "Bearer token"} + await transport._initialize_session(headers=test_headers) + + assert transport._send_request.call_count == 2 + + init_call = transport._send_request.call_args_list[0] + assert isinstance(init_call.kwargs["request"], types.InitializeRequest) + assert init_call.kwargs["headers"] == test_headers + + notify_call = transport._send_request.call_args_list[1] + assert isinstance( + notify_call.kwargs["request"], types.InitializedNotification + ) + assert notify_call.kwargs["headers"] == test_headers + + # --- Tool Management Tests --- async def test_tools_list_success(self, transport, mocker): From 6b091c16adf0a670e1ae1ecc4ef68eba02a0512d Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 27 Jan 2026 12:35:48 +0530 Subject: [PATCH 13/18] fix merge issues --- .../toolbox-core/integration.cloudbuild.yaml | 24 +++++++++---------- .../integration.cloudbuild.yaml | 24 +++++++++---------- .../integration.cloudbuild.yaml | 24 +++++++++---------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/toolbox-core/integration.cloudbuild.yaml b/packages/toolbox-core/integration.cloudbuild.yaml index 1fc30020..6c346f83 100644 --- a/packages/toolbox-core/integration.cloudbuild.yaml +++ b/packages/toolbox-core/integration.cloudbuild.yaml @@ -14,11 +14,11 @@ steps: - id: Install library requirements - name: "python:${_VERSION}" - dir: "packages/toolbox-core" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-core' env: args: - - "-c" + - '-c' - | pip install uv uv venv /workspace/venv @@ -27,25 +27,25 @@ steps: uv pip install -r requirements.txt entrypoint: /bin/bash - id: Install test requirements - name: "python:${_VERSION}" - dir: "packages/toolbox-core" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-core' env: args: - - "-c" + - '-c' - | source /workspace/venv/bin/activate uv pip install -e '.[test]' entrypoint: /bin/bash - id: Run integration tests - name: "python:${_VERSION}" - dir: "packages/toolbox-core" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-core' env: - TOOLBOX_URL=$_TOOLBOX_URL - TOOLBOX_VERSION=$_TOOLBOX_VERSION - GOOGLE_CLOUD_PROJECT=$PROJECT_ID - TOOLBOX_MANIFEST_VERSION=${_TOOLBOX_MANIFEST_VERSION} args: - - "-c" + - '-c' - | source /workspace/venv/bin/activate python -m pytest --cov=src/toolbox_core --cov-report=term --cov-fail-under=90 tests/ @@ -53,6 +53,6 @@ steps: options: logging: CLOUD_LOGGING_ONLY substitutions: - _VERSION: "3.13" - _TOOLBOX_VERSION: "0.26.0" - _TOOLBOX_MANIFEST_VERSION: "34" + _VERSION: '3.13' + _TOOLBOX_VERSION: '0.26.0' + _TOOLBOX_MANIFEST_VERSION: '34' diff --git a/packages/toolbox-langchain/integration.cloudbuild.yaml b/packages/toolbox-langchain/integration.cloudbuild.yaml index 74f0e582..adc59d87 100644 --- a/packages/toolbox-langchain/integration.cloudbuild.yaml +++ b/packages/toolbox-langchain/integration.cloudbuild.yaml @@ -14,11 +14,11 @@ steps: - id: Install library requirements - name: "python:${_VERSION}" - dir: "packages/toolbox-langchain" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-langchain' env: args: - - "-c" + - '-c' - | pip install uv uv venv /workspace/venv @@ -27,25 +27,25 @@ steps: uv pip install -r requirements.txt entrypoint: /bin/bash - id: Install test requirements - name: "python:${_VERSION}" - dir: "packages/toolbox-langchain" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-langchain' env: args: - - "-c" + - '-c' - | source /workspace/venv/bin/activate uv pip install -e '.[test]' entrypoint: /bin/bash - id: Run integration tests - name: "python:${_VERSION}" - dir: "packages/toolbox-langchain" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-langchain' env: - TOOLBOX_URL=$_TOOLBOX_URL - TOOLBOX_VERSION=$_TOOLBOX_VERSION - GOOGLE_CLOUD_PROJECT=$PROJECT_ID - TOOLBOX_MANIFEST_VERSION=${_TOOLBOX_MANIFEST_VERSION} args: - - "-c" + - '-c' - | source /workspace/venv/bin/activate python -m pytest --cov=src/toolbox_langchain --cov-report=term --cov-fail-under=90 tests/ @@ -53,6 +53,6 @@ steps: options: logging: CLOUD_LOGGING_ONLY substitutions: - _VERSION: "3.13" - _TOOLBOX_VERSION: "0.26.0" - _TOOLBOX_MANIFEST_VERSION: "34" + _VERSION: '3.13' + _TOOLBOX_VERSION: '0.26.0' + _TOOLBOX_MANIFEST_VERSION: '34' diff --git a/packages/toolbox-llamaindex/integration.cloudbuild.yaml b/packages/toolbox-llamaindex/integration.cloudbuild.yaml index 3549aae3..e723a655 100644 --- a/packages/toolbox-llamaindex/integration.cloudbuild.yaml +++ b/packages/toolbox-llamaindex/integration.cloudbuild.yaml @@ -14,11 +14,11 @@ steps: - id: Install library requirements - name: "python:${_VERSION}" - dir: "packages/toolbox-llamaindex" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-llamaindex' env: args: - - "-c" + - '-c' - | pip install uv uv venv /workspace/venv @@ -27,25 +27,25 @@ steps: uv pip install -r requirements.txt entrypoint: /bin/bash - id: Install test requirements - name: "python:${_VERSION}" - dir: "packages/toolbox-llamaindex" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-llamaindex' env: args: - - "-c" + - '-c' - | source /workspace/venv/bin/activate uv pip install -e '.[test]' entrypoint: /bin/bash - id: Run integration tests - name: "python:${_VERSION}" - dir: "packages/toolbox-llamaindex" + name: 'python:${_VERSION}' + dir: 'packages/toolbox-llamaindex' env: - TOOLBOX_URL=$_TOOLBOX_URL - TOOLBOX_VERSION=$_TOOLBOX_VERSION - GOOGLE_CLOUD_PROJECT=$PROJECT_ID - TOOLBOX_MANIFEST_VERSION=${_TOOLBOX_MANIFEST_VERSION} args: - - "-c" + - '-c' - | source /workspace/venv/bin/activate python -m pytest --cov=src/toolbox_llamaindex --cov-report=term --cov-fail-under=90 tests/ @@ -53,6 +53,6 @@ steps: options: logging: CLOUD_LOGGING_ONLY substitutions: - _VERSION: "3.13" - _TOOLBOX_VERSION: "0.26.0" - _TOOLBOX_MANIFEST_VERSION: "34" + _VERSION: '3.13' + _TOOLBOX_VERSION: '0.26.0' + _TOOLBOX_MANIFEST_VERSION: '34' From 6e3ff29c07ea5f0866f427c89271b439c1beba60 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 27 Jan 2026 12:37:22 +0530 Subject: [PATCH 14/18] lint --- packages/toolbox-core/tests/mcp_transport/test_v20241105.py | 4 +--- packages/toolbox-core/tests/mcp_transport/test_v20250326.py | 4 +--- packages/toolbox-core/tests/mcp_transport/test_v20250618.py | 4 +--- packages/toolbox-core/tests/mcp_transport/test_v20251125.py | 5 +---- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20241105.py b/packages/toolbox-core/tests/mcp_transport/test_v20241105.py index c1368877..23e0d180 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20241105.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20241105.py @@ -232,9 +232,7 @@ async def test_initialize_passes_headers_to_request(self, transport): assert init_call.kwargs["headers"] == test_headers notify_call = transport._send_request.call_args_list[1] - assert isinstance( - notify_call.kwargs["request"], types.InitializedNotification - ) + assert isinstance(notify_call.kwargs["request"], types.InitializedNotification) assert notify_call.kwargs["headers"] == test_headers # --- Tool Management Tests --- diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20250326.py b/packages/toolbox-core/tests/mcp_transport/test_v20250326.py index de0808f3..4192dcd6 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20250326.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20250326.py @@ -237,9 +237,7 @@ async def test_initialize_passes_headers_to_request(self, transport): assert init_call.kwargs["headers"] == test_headers notify_call = transport._send_request.call_args_list[1] - assert isinstance( - notify_call.kwargs["request"], types.InitializedNotification - ) + assert isinstance(notify_call.kwargs["request"], types.InitializedNotification) assert notify_call.kwargs["headers"] == test_headers # --- Tool Management Tests --- diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20250618.py b/packages/toolbox-core/tests/mcp_transport/test_v20250618.py index 7c4b4d6c..29b06ac4 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20250618.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20250618.py @@ -240,9 +240,7 @@ async def test_initialize_passes_headers_to_request(self, transport): assert init_call.kwargs["headers"] == test_headers notify_call = transport._send_request.call_args_list[1] - assert isinstance( - notify_call.kwargs["request"], types.InitializedNotification - ) + assert isinstance(notify_call.kwargs["request"], types.InitializedNotification) assert notify_call.kwargs["headers"] == test_headers # --- Tool Management Tests --- diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py index bfde63bc..af70fc8a 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -240,12 +240,9 @@ async def test_initialize_passes_headers_to_request(self, transport): assert init_call.kwargs["headers"] == test_headers notify_call = transport._send_request.call_args_list[1] - assert isinstance( - notify_call.kwargs["request"], types.InitializedNotification - ) + assert isinstance(notify_call.kwargs["request"], types.InitializedNotification) assert notify_call.kwargs["headers"] == test_headers - # --- Tool Management Tests --- async def test_tools_list_success(self, transport, mocker): From 6e0f1266bf77eca55ad6048466da826265e50737 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 27 Jan 2026 12:52:53 +0530 Subject: [PATCH 15/18] fix e2e tests --- 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 c644136f..6acaeade 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -30,7 +30,7 @@ ) async def toolbox(request): """Creates a ToolboxClient instance shared by all tests in this module.""" - toolbox = ToolboxClient("http://localhost:5000", protocol=request.param) + toolbox = ToolboxClient("http://localhost:5000", protocol=Protocol(request.param)) try: yield toolbox finally: From 9e1e7fa130c142a7ea0457156e850dbeb850972f Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 27 Jan 2026 15:22:49 +0530 Subject: [PATCH 16/18] test: relax MCP protocol version check in client init tests --- .../toolbox-langchain/tests/test_async_client.py | 14 ++++++++------ packages/toolbox-langchain/tests/test_client.py | 10 +++++++--- .../toolbox-llamaindex/tests/test_async_client.py | 14 ++++++++------ packages/toolbox-llamaindex/tests/test_client.py | 10 +++++++--- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/toolbox-langchain/tests/test_async_client.py b/packages/toolbox-langchain/tests/test_async_client.py index ced88bba..7be61889 100644 --- a/packages/toolbox-langchain/tests/test_async_client.py +++ b/packages/toolbox-langchain/tests/test_async_client.py @@ -348,9 +348,11 @@ async def test_init_with_client_headers( """Tests that client_headers are passed to the core client during initialization.""" headers = {"X-Test-Header": "value"} AsyncToolboxClient(URL, session=mock_session, client_headers=headers) - mock_core_client_constructor.assert_called_once_with( - url=URL, - session=mock_session, - client_headers=headers, - protocol=Protocol.MCP_v20250618, - ) + + mock_core_client_constructor.assert_called_once() + call_kwargs = mock_core_client_constructor.call_args[1] + + assert call_kwargs["url"] == URL + assert call_kwargs["session"] == mock_session + assert call_kwargs["client_headers"] == headers + assert call_kwargs["protocol"] in Protocol.get_supported_mcp_versions() diff --git a/packages/toolbox-langchain/tests/test_client.py b/packages/toolbox-langchain/tests/test_client.py index 4ec8d52e..a32aeb82 100644 --- a/packages/toolbox-langchain/tests/test_client.py +++ b/packages/toolbox-langchain/tests/test_client.py @@ -490,9 +490,13 @@ def test_init_with_client_headers(self, mock_core_client_constructor): """Tests that client_headers are passed to the core client during initialization.""" headers = {"X-Test-Header": "value"} ToolboxClient(URL, client_headers=headers) - mock_core_client_constructor.assert_called_once_with( - url=URL, client_headers=headers, protocol=Protocol.MCP_v20250618 - ) + + mock_core_client_constructor.assert_called_once() + call_kwargs = mock_core_client_constructor.call_args[1] + + assert call_kwargs["url"] == URL + assert call_kwargs["client_headers"] == headers + assert call_kwargs["protocol"] in Protocol.get_supported_mcp_versions() @patch("toolbox_langchain.client.ToolboxCoreSyncClient") def test_context_manager(self, mock_core_client_constructor): diff --git a/packages/toolbox-llamaindex/tests/test_async_client.py b/packages/toolbox-llamaindex/tests/test_async_client.py index 1f295172..9875dfb0 100644 --- a/packages/toolbox-llamaindex/tests/test_async_client.py +++ b/packages/toolbox-llamaindex/tests/test_async_client.py @@ -348,9 +348,11 @@ async def test_init_with_client_headers( """Tests that client_headers are passed to the core client during initialization.""" headers = {"X-Test-Header": "value"} AsyncToolboxClient(URL, session=mock_session, client_headers=headers) - mock_core_client_constructor.assert_called_once_with( - url=URL, - session=mock_session, - client_headers=headers, - protocol=Protocol.MCP_v20250618, - ) + + mock_core_client_constructor.assert_called_once() + call_kwargs = mock_core_client_constructor.call_args[1] + + assert call_kwargs["url"] == URL + assert call_kwargs["session"] == mock_session + assert call_kwargs["client_headers"] == headers + assert call_kwargs["protocol"] in Protocol.get_supported_mcp_versions() diff --git a/packages/toolbox-llamaindex/tests/test_client.py b/packages/toolbox-llamaindex/tests/test_client.py index 8af39123..60f510b6 100644 --- a/packages/toolbox-llamaindex/tests/test_client.py +++ b/packages/toolbox-llamaindex/tests/test_client.py @@ -429,9 +429,13 @@ def test_init_with_client_headers(self, mock_core_client_constructor): """Tests that client_headers are passed to the core client during initialization.""" headers = {"X-Test-Header": "value"} ToolboxClient(URL, client_headers=headers) - mock_core_client_constructor.assert_called_once_with( - url=URL, client_headers=headers, protocol=Protocol.MCP_v20250618 - ) + + mock_core_client_constructor.assert_called_once() + call_kwargs = mock_core_client_constructor.call_args[1] + + assert call_kwargs["url"] == URL + assert call_kwargs["client_headers"] == headers + assert call_kwargs["protocol"] in Protocol.get_supported_mcp_versions() @patch("toolbox_llamaindex.client.ToolboxCoreSyncClient") def test_context_manager(self, mock_core_client_constructor): From a4e8084cf6a3a19a34ef352bf74b9b910c1ff55a Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:27:18 +0530 Subject: [PATCH 17/18] Update mcp.py --- .../src/toolbox_core/mcp_transport/v20251125/mcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f7ce9958..73cccb69 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 @@ -188,4 +188,4 @@ async def tool_invoke( f"Failed to invoke tool '{tool_name}': No response from server." ) - return "".join(c.text for c in result.content if c.type == "text") or "null" + return self._process_tool_result_content(result.content) From af7e98babc836d015f2a45f717c21785b3b20bfb Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 27 Jan 2026 15:30:17 +0530 Subject: [PATCH 18/18] add tests --- .../tests/mcp_transport/test_v20251125.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py index af70fc8a..16100baa 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -302,3 +302,58 @@ async def test_tool_get_success(self, transport, mocker): transport._server_version = "1.0" manifest = await transport.tool_get("get_weather") assert "get_weather" in manifest.tools + + async def test_tool_invoke_multiple_json_objects(self, transport, mocker): + mocker.patch.object(transport, "_ensure_initialized", new_callable=AsyncMock) + mock_response = types.CallToolResult( + content=[ + types.TextContent(type="text", text='{"foo":"bar", "baz": "qux"}'), + types.TextContent(type="text", text='{"foo":"quux", "baz":"corge"}'), + ] + ) + + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=mock_response, + ) + result = await transport.tool_invoke("tool", {}, {}) + expected = '[{"foo":"bar", "baz": "qux"},{"foo":"quux", "baz":"corge"}]' + assert result == expected + + async def test_tool_invoke_split_text(self, transport, mocker): + mocker.patch.object(transport, "_ensure_initialized", new_callable=AsyncMock) + mock_response = types.CallToolResult( + content=[ + types.TextContent(type="text", text="Hello "), + types.TextContent(type="text", text="World"), + ] + ) + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=mock_response, + ) + + result = await transport.tool_invoke("tool", {}, {}) + assert result == "Hello World" + + async def test_tool_invoke_split_json_object(self, transport, mocker): + mocker.patch.object(transport, "_ensure_initialized", new_callable=AsyncMock) + mock_response = types.CallToolResult( + content=[ + types.TextContent(type="text", text='{"a": '), + types.TextContent(type="text", text="1}"), + ] + ) + mocker.patch.object( + transport, + "_send_request", + new_callable=AsyncMock, + return_value=mock_response, + ) + + result = await transport.tool_invoke("tool", {}, {}) + assert result == '{"a": 1}'