diff --git a/packages/toolbox-core/src/toolbox_core/itransport.py b/packages/toolbox-core/src/toolbox_core/itransport.py index 7f392ed85..cae231ee1 100644 --- a/packages/toolbox-core/src/toolbox_core/itransport.py +++ b/packages/toolbox-core/src/toolbox_core/itransport.py @@ -52,6 +52,7 @@ async def tool_invoke( arguments: dict, headers: Mapping[str, str], telemetry_attributes: Optional[TelemetryAttributes] = None, + secure_arguments: Optional[dict] = None, ) -> str: """Invokes a specific tool on the server.""" pass diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/transport_base.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/transport_base.py index e9e7c473b..781278c48 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/transport_base.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/transport_base.py @@ -205,9 +205,21 @@ def _convert_tool_schema(self, tool_data: dict) -> ToolSchema: parameters.append(param_schema) + secure_parameters = [] + secure_input_schema = tool_data.get("secureInputSchema") + if isinstance(secure_input_schema, dict): + sec_properties = secure_input_schema.get("properties", {}) + sec_required = secure_input_schema.get("required", []) + for name, schema in sec_properties.items(): + param_schema = self._convert_parameter_schema( + name, schema, sec_required + ) + secure_parameters.append(param_schema) + return ToolSchema( description=tool_data.get("description") or "", parameters=parameters, + secure_parameters=secure_parameters, authRequired=invoke_auth, ) diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20241105/mcp.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20241105/mcp.py index 8344c501b..8e1262821 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20241105/mcp.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20241105/mcp.py @@ -341,8 +341,15 @@ async def tool_invoke( arguments: dict, headers: Optional[Mapping[str, str]], telemetry_attributes: Optional[TelemetryAttributes] = None, + secure_arguments: Optional[dict] = None, ) -> str: """Invokes a specific tool on the server using the MCP protocol.""" + if secure_arguments: + raise NotImplementedError( + f"Secure parameters are not supported in MCP protocol version '{self._protocol_version}'. " + "Please use protocol version '2026-07-28' or newer." + ) + await self._ensure_initialized(headers=headers) payload = self._build_telemetry_payload(telemetry_attributes) diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250326/mcp.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250326/mcp.py index 3d8ae24f8..aecff9ada 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250326/mcp.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250326/mcp.py @@ -368,8 +368,15 @@ async def tool_invoke( arguments: dict, headers: Optional[Mapping[str, str]], telemetry_attributes: Optional[TelemetryAttributes] = None, + secure_arguments: Optional[dict] = None, ) -> str: """Invokes a specific tool on the server using the MCP protocol.""" + if secure_arguments: + raise NotImplementedError( + f"Secure parameters are not supported in MCP protocol version '{self._protocol_version}'. " + "Please use protocol version '2026-07-28' or newer." + ) + await self._ensure_initialized(headers=headers) payload = self._build_telemetry_payload(telemetry_attributes) diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py index b9210b45c..7a9611990 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py @@ -349,8 +349,15 @@ async def tool_invoke( arguments: dict, headers: Optional[Mapping[str, str]], telemetry_attributes: Optional[TelemetryAttributes] = None, + secure_arguments: Optional[dict] = None, ) -> str: """Invokes a specific tool on the server using the MCP protocol.""" + if secure_arguments: + raise NotImplementedError( + f"Secure parameters are not supported in MCP protocol version '{self._protocol_version}'. " + "Please use protocol version '2026-07-28' or newer." + ) + await self._ensure_initialized(headers=headers) payload = self._build_telemetry_payload(telemetry_attributes) 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 6ba126caa..ec78212f8 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 @@ -349,8 +349,15 @@ async def tool_invoke( arguments: dict, headers: Optional[Mapping[str, str]], telemetry_attributes: Optional[TelemetryAttributes] = None, + secure_arguments: Optional[dict] = None, ) -> str: """Invokes a specific tool on the server using the MCP protocol.""" + if secure_arguments: + raise NotImplementedError( + f"Secure parameters are not supported in MCP protocol version '{self._protocol_version}'. " + "Please use protocol version '2026-07-28' or newer." + ) + await self._ensure_initialized(headers=headers) payload = self._build_telemetry_payload(telemetry_attributes) diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260728/mcp.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260728/mcp.py index 99a10fc22..af7eb334f 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260728/mcp.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260728/mcp.py @@ -293,6 +293,7 @@ async def tool_invoke( arguments: dict, headers: Optional[Mapping[str, str]], telemetry_attributes: Optional[TelemetryAttributes] = None, + secure_arguments: Optional[dict] = None, ) -> str: """Invokes a specific tool on the server using the MCP protocol.""" await self._ensure_initialized(headers=headers) @@ -332,7 +333,10 @@ async def tool_invoke( url=self._mcp_base_url, request=types.CallToolRequest( params=types.CallToolRequestParams( - name=tool_name, arguments=arguments, field_meta=meta + name=tool_name, + arguments=arguments, + secure_arguments=secure_arguments or None, + field_meta=meta, ) ), headers=headers, diff --git a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260728/types.py b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260728/types.py index da1e1df5c..7e388be38 100644 --- a/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260728/types.py +++ b/packages/toolbox-core/src/toolbox_core/mcp_transport/v20260728/types.py @@ -74,7 +74,9 @@ class ClientCapabilities(_BaseMCPModel): roots: dict[str, Any] | None = None sampling: SamplingCapabilities | None = None elicitation: ElicitationCapabilities | None = None - extensions: dict[str, Any] | None = None + extensions: dict[str, Any] | None = Field( + default_factory=lambda: {"com.google.cloud/toolbox.v1": {}} + ) class Implementation(_BaseMCPModel): @@ -166,6 +168,9 @@ def get_result_model(self) -> Type[ListToolsResult]: class CallToolRequestParams(_BaseMCPModel): name: str arguments: dict[str, Any] + secure_arguments: dict[str, Any] | None = Field( + default=None, serialization_alias="secureArguments" + ) field_meta: MCPMeta = Field(..., serialization_alias="_meta") diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index 59bd0788d..9e8dae960 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -187,6 +187,7 @@ class ToolSchema(BaseModel): description: str parameters: list[ParameterSchema] + secure_parameters: list[ParameterSchema] = [] authRequired: list[str] = [] diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20241105.py b/packages/toolbox-core/tests/mcp_transport/test_v20241105.py index 7a0c58bc3..23bbbf059 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20241105.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20241105.py @@ -590,3 +590,15 @@ async def test_version_negotiation_legacy_string_fallback(self, transport): with pytest.raises(RuntimeError, match="no fallback versions"): await transport._send_request("http://test.local/messages", request) + + async def test_tool_invoke_rejects_secure_arguments(self, transport): + with pytest.raises( + NotImplementedError, + match="Secure parameters are not supported in MCP protocol version '2024-11-05'", + ): + await transport.tool_invoke( + "my_tool", + {"arg": "val"}, + headers=None, + secure_arguments={"secret": "value"}, + ) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20250326.py b/packages/toolbox-core/tests/mcp_transport/test_v20250326.py index 921d3aa47..a17ad2d80 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20250326.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20250326.py @@ -588,3 +588,15 @@ async def test_version_negotiation_legacy_string_fallback(self, transport): with pytest.raises(ProtocolNegotiationError): await transport._send_request("http://test.local/messages", request) + + async def test_tool_invoke_rejects_secure_arguments(self, transport): + with pytest.raises( + NotImplementedError, + match="Secure parameters are not supported in MCP protocol version '2025-03-26'", + ): + await transport.tool_invoke( + "my_tool", + {"arg": "val"}, + headers=None, + secure_arguments={"secret": "value"}, + ) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20250618.py b/packages/toolbox-core/tests/mcp_transport/test_v20250618.py index aeebcd546..077876476 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20250618.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20250618.py @@ -624,3 +624,15 @@ async def test_version_negotiation_legacy_string_fallback(self, transport): with pytest.raises(ProtocolNegotiationError): await transport._send_request("http://test.local/messages", request) + + async def test_tool_invoke_rejects_secure_arguments(self, transport): + with pytest.raises( + NotImplementedError, + match="Secure parameters are not supported in MCP protocol version '2025-06-18'", + ): + await transport.tool_invoke( + "my_tool", + {"arg": "val"}, + headers=None, + secure_arguments={"secret": "value"}, + ) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py index a37fa47a6..f90a3d808 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20251125.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20251125.py @@ -608,3 +608,15 @@ async def test_version_negotiation_legacy_string_fallback(self, transport): with pytest.raises(ProtocolNegotiationError): await transport._send_request("http://test.local/messages", request) + + async def test_tool_invoke_rejects_secure_arguments(self, transport): + with pytest.raises( + NotImplementedError, + match="Secure parameters are not supported in MCP protocol version '2025-11-25'", + ): + await transport.tool_invoke( + "my_tool", + {"arg": "val"}, + headers=None, + secure_arguments={"secret": "value"}, + ) diff --git a/packages/toolbox-core/tests/mcp_transport/test_v20260728.py b/packages/toolbox-core/tests/mcp_transport/test_v20260728.py index 94146e641..55925d553 100644 --- a/packages/toolbox-core/tests/mcp_transport/test_v20260728.py +++ b/packages/toolbox-core/tests/mcp_transport/test_v20260728.py @@ -575,3 +575,91 @@ async def test_result_type_parsing_and_fallback(self, transport): {"tools": [], "resultType": "input_required"} ) assert res_custom.result_type == "input_required" + + async def test_client_capabilities_secure_parameters(self): + """Test that ClientCapabilities advertises toolbox.v1 secure_parameters support.""" + caps = types.ClientCapabilities() + assert caps.extensions is not None + assert "com.google.cloud/toolbox.v1" in caps.extensions + assert caps.extensions["com.google.cloud/toolbox.v1"] == {} + + async def test_tools_list_parses_secure_input_schema(self, transport): + """Test that secureInputSchema is parsed into ToolSchema.secure_parameters.""" + 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": { + "tools": [ + { + "name": "secure_tool", + "description": "Tool with secure input schema", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "search query", + } + }, + "required": ["query"], + }, + "secureInputSchema": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "description": "Secret API Key", + } + }, + "required": ["api_key"], + }, + } + ] + }, + } + transport._session.post.return_value.__aenter__.return_value = mock_response + + manifest = await transport.tools_list() + assert "secure_tool" in manifest.tools + tool = manifest.tools["secure_tool"] + assert len(tool.parameters) == 1 + assert tool.parameters[0].name == "query" + assert len(tool.secure_parameters) == 1 + assert tool.secure_parameters[0].name == "api_key" + assert tool.secure_parameters[0].required is True + assert tool.secure_parameters[0].description == "Secret API Key" + + async def test_tool_invoke_with_secure_arguments(self, transport): + """Test that tool_invoke sends secure_arguments in CallToolRequestParams.""" + 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": {"content": [{"type": "text", "text": "invocation success"}]}, + } + transport._session.post.return_value.__aenter__.return_value = mock_response + + result = await transport.tool_invoke( + "secure_tool", + {"query": "search term"}, + {"custom-header": "value"}, + secure_arguments={"api_key": "sec-val-999"}, + ) + assert result == "invocation success" + + call_args = transport._session.post.call_args + sent_json = call_args.kwargs["json"] + assert sent_json["method"] == "tools/call" + params = sent_json["params"] + assert params["name"] == "secure_tool" + assert params["arguments"] == {"query": "search term"} + assert params["secureArguments"] == {"api_key": "sec-val-999"}