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..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 @@ -67,7 +68,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..73cccb69 --- /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 self._process_tool_result_content(result.content) 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..16100baa --- /dev/null +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -0,0 +1,359 @@ +# 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, 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() + + 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): + 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 + + 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}' diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index 37f6c751..6acaeade 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -26,15 +26,11 @@ @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.""" - toolbox = ToolboxClient("http://localhost:5000", protocol=request.param) + toolbox = ToolboxClient("http://localhost:5000", protocol=Protocol(request.param)) try: yield toolbox finally: 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 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-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. |