Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/toolbox-core/src/toolbox_core/itransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")


Expand Down
1 change: 1 addition & 0 deletions packages/toolbox-core/src/toolbox_core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class ToolSchema(BaseModel):

description: str
parameters: list[ParameterSchema]
secure_parameters: list[ParameterSchema] = []
authRequired: list[str] = []


Expand Down
12 changes: 12 additions & 0 deletions packages/toolbox-core/tests/mcp_transport/test_v20241105.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
)
12 changes: 12 additions & 0 deletions packages/toolbox-core/tests/mcp_transport/test_v20250326.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
)
12 changes: 12 additions & 0 deletions packages/toolbox-core/tests/mcp_transport/test_v20250618.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
)
12 changes: 12 additions & 0 deletions packages/toolbox-core/tests/mcp_transport/test_v20251125.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
)
88 changes: 88 additions & 0 deletions packages/toolbox-core/tests/mcp_transport/test_v20260728.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Loading