Skip to content

Commit c1a4669

Browse files
committed
refactor: move transport logic to a ToolboxTransport class (#344)
* add basic code * fixes * test fix * new unit tests * rename ToolboxTransport * add py3.9 support * fix langchain tool tests * test fix * lint * fix tests * move manage session into transport * move warning to diff file * avoid code duplication * fix tests * lint * remove redundant tests * make invoke method return str * lint * fix return type * small refactor * rename private method * fix tests * lint
1 parent 8851555 commit c1a4669

File tree

2 files changed

+43
-179
lines changed

2 files changed

+43
-179
lines changed

packages/toolbox-core/src/toolbox_core/client.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,10 @@
2020
from deprecated import deprecated
2121

2222
from .itransport import ITransport
23-
from .mcp_transport import (
24-
McpHttpTransport_v20241105,
25-
McpHttpTransport_v20250326,
26-
McpHttpTransport_v20250618,
27-
)
28-
from .protocol import Protocol, ToolSchema
23+
from .protocol import ToolSchema
2924
from .tool import ToolboxTool
3025
from .toolbox_transport import ToolboxTransport
26+
from .toolbox_transport import ToolboxTransport
3127
from .utils import identify_auth_requirements, resolve_value
3228

3329

@@ -40,6 +36,7 @@ class ToolboxClient:
4036
is not provided.
4137
"""
4238

39+
__transport: ITransport
4340
__transport: ITransport
4441

4542
def __init__(
@@ -60,22 +57,10 @@ def __init__(
6057
If None (default), a new session is created internally. Note that
6158
if a session is provided, its lifecycle (including closing)
6259
should typically be managed externally.
63-
client_headers: Headers to include in each request sent through this
64-
client.
65-
protocol: The communication protocol to use.
60+
client_headers: Headers to include in each request sent through this client.
6661
"""
67-
if protocol == Protocol.TOOLBOX:
68-
self.__transport = ToolboxTransport(url, session)
69-
elif protocol in Protocol.get_supported_mcp_versions():
70-
if protocol == Protocol.MCP_v20250618:
71-
self.__transport = McpHttpTransport_v20250618(url, session, protocol)
72-
elif protocol == Protocol.MCP_v20250326:
73-
self.__transport = McpHttpTransport_v20250326(url, session, protocol)
74-
elif protocol == Protocol.MCP_v20241105:
75-
self.__transport = McpHttpTransport_v20241105(url, session, protocol)
76-
else:
77-
raise ValueError(f"Unsupported MCP protocol version: {protocol}")
7862

63+
self.__transport = ToolboxTransport(url, session)
7964
self.__client_headers = client_headers if client_headers is not None else {}
8065

8166
def __parse_tool(
@@ -114,6 +99,7 @@ def __parse_tool(
11499
)
115100

116101
tool = ToolboxTool(
102+
transport=self.__transport,
117103
transport=self.__transport,
118104
name=name,
119105
description=schema.description,
@@ -160,6 +146,7 @@ async def close(self):
160146
is responsible for its lifecycle.
161147
"""
162148
await self.__transport.close()
149+
await self.__transport.close()
163150

164151
async def load_tool(
165152
self,
@@ -200,6 +187,7 @@ async def load_tool(
200187
for name, val in self.__client_headers.items()
201188
}
202189

190+
manifest = await self.__transport.tool_get(name, resolved_headers)
203191
manifest = await self.__transport.tool_get(name, resolved_headers)
204192

205193
# parse the provided definition to a tool
@@ -277,6 +265,8 @@ async def load_toolset(
277265

278266
manifest = await self.__transport.tools_list(name, resolved_headers)
279267

268+
manifest = await self.__transport.tools_list(name, resolved_headers)
269+
280270
tools: list[ToolboxTool] = []
281271
overall_used_auth_keys: set[str] = set()
282272
overall_used_bound_params: set[str] = set()

packages/toolbox-core/tests/test_tool.py

Lines changed: 33 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
from toolbox_core.tool import ToolboxTool
3030
from toolbox_core.toolbox_transport import ToolboxTransport
3131
from toolbox_core.utils import create_func_docstring, resolve_value
32+
from toolbox_core.tool import ToolboxTool
33+
from toolbox_core.toolbox_transport import ToolboxTransport
34+
from toolbox_core.utils import create_func_docstring, resolve_value
3235

3336
TEST_BASE_URL = "http://toolbox.example.com"
3437
HTTPS_BASE_URL = "https://toolbox.example.com"
@@ -113,7 +116,9 @@ def toolbox_tool(
113116
) -> ToolboxTool:
114117
"""Fixture for a ToolboxTool instance with common test setup."""
115118
transport = ToolboxTransport(TEST_BASE_URL, http_session)
119+
transport = ToolboxTransport(TEST_BASE_URL, http_session)
116120
return ToolboxTool(
121+
transport=transport,
117122
transport=transport,
118123
name=TEST_TOOL_NAME,
119124
description=sample_tool_description,
@@ -232,8 +237,10 @@ async def test_tool_creation_callable_and_run(
232237
with aioresponses() as m:
233238
m.post(invoke_url, status=200, payload=mock_server_response_body)
234239
transport = ToolboxTransport(base_url, http_session)
240+
transport = ToolboxTransport(base_url, http_session)
235241

236242
tool_instance = ToolboxTool(
243+
transport=transport,
237244
transport=transport,
238245
name=tool_name,
239246
description=sample_tool_description,
@@ -278,8 +285,10 @@ async def test_tool_run_with_pydantic_validation_error(
278285
with aioresponses() as m:
279286
m.post(invoke_url, status=200, payload={"result": "Should not be called"})
280287
transport = ToolboxTransport(base_url, http_session)
288+
transport = ToolboxTransport(base_url, http_session)
281289

282290
tool_instance = ToolboxTool(
291+
transport=transport,
283292
transport=transport,
284293
name=tool_name,
285294
description=sample_tool_description,
@@ -369,8 +378,10 @@ def test_tool_init_basic(http_session, sample_tool_params, sample_tool_descripti
369378
with catch_warnings(record=True) as record:
370379
simplefilter("always")
371380
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
381+
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
372382

373383
tool_instance = ToolboxTool(
384+
transport=transport,
374385
transport=transport,
375386
name=TEST_TOOL_NAME,
376387
description=sample_tool_description,
@@ -399,7 +410,9 @@ def test_tool_init_with_client_headers(
399410
):
400411
"""Tests tool initialization *with* client headers."""
401412
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
413+
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
402414
tool_instance = ToolboxTool(
415+
transport=transport,
403416
transport=transport,
404417
name=TEST_TOOL_NAME,
405418
description=sample_tool_description,
@@ -423,7 +436,9 @@ def test_tool_add_auth_token_getters_conflict_with_existing_client_header(
423436
whose token name conflicts with an existing client header.
424437
"""
425438
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
439+
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
426440
tool_instance = ToolboxTool(
441+
transport=transport,
427442
transport=transport,
428443
name="tool_with_client_header",
429444
description=sample_tool_description,
@@ -473,6 +488,21 @@ async def test_auth_token_overrides_client_header(
473488
"X-Another-Header": "another-value",
474489
},
475490
)
491+
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
492+
tool_instance = ToolboxTool(
493+
transport=transport,
494+
name=TEST_TOOL_NAME,
495+
description=sample_tool_description,
496+
params=sample_tool_params,
497+
required_authn_params={},
498+
required_authz_tokens=[],
499+
auth_service_token_getters={"test-auth": lambda: "value-from-auth-getter-123"},
500+
bound_params={},
501+
client_headers={
502+
"test-auth_token": "value-from-client",
503+
"X-Another-Header": "another-value",
504+
},
505+
)
476506
tool_name = TEST_TOOL_NAME
477507
base_url = HTTPS_BASE_URL
478508
invoke_url = f"{base_url}/api/tool/{tool_name}/invoke"
@@ -490,6 +520,7 @@ async def test_auth_token_overrides_client_header(
490520
method="POST",
491521
json=input_args,
492522
headers={
523+
"test-auth_token": "value-from-auth-getter-123",
493524
"test-auth_token": "value-from-auth-getter-123",
494525
"X-Another-Header": "another-value",
495526
},
@@ -507,7 +538,9 @@ def test_add_auth_token_getter_unused_token(
507538
an unused authentication service.
508539
"""
509540
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
541+
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
510542
tool_instance = ToolboxTool(
543+
transport=transport,
511544
transport=transport,
512545
name=TEST_TOOL_NAME,
513546
description=sample_tool_description,
@@ -526,162 +559,3 @@ def test_add_auth_token_getter_unused_token(
526559
next(iter(unused_auth_getters)),
527560
unused_auth_getters[next(iter(unused_auth_getters))],
528561
)
529-
530-
531-
# --- Tests for Parameter Binding ---
532-
533-
534-
@pytest.mark.asyncio
535-
async def test_bind_param_success(
536-
http_session: ClientSession,
537-
sample_tool_params: list[ParameterSchema],
538-
sample_tool_description: str,
539-
):
540-
"""
541-
Tests successfully binding a single parameter with a static value using bind_param.
542-
"""
543-
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
544-
original_tool = ToolboxTool(
545-
transport=transport,
546-
name=TEST_TOOL_NAME,
547-
description=sample_tool_description,
548-
params=sample_tool_params,
549-
required_authn_params={},
550-
required_authz_tokens=[],
551-
auth_service_token_getters={},
552-
bound_params={},
553-
client_headers={},
554-
)
555-
556-
# Bind the 'count' parameter
557-
bound_tool = original_tool.bind_param("count", 100)
558-
559-
# Assert immutability and changes
560-
assert bound_tool is not original_tool
561-
assert "count" not in bound_tool.__signature__.parameters
562-
assert "message" in bound_tool.__signature__.parameters
563-
assert "count" in original_tool.__signature__.parameters
564-
assert bound_tool._bound_params == {"count": 100}
565-
assert original_tool._bound_params == {}
566-
567-
# Test invocation of the new tool
568-
invoke_url = f"{HTTPS_BASE_URL}/api/tool/{TEST_TOOL_NAME}/invoke"
569-
with aioresponses() as m:
570-
m.post(invoke_url, status=200, payload={"result": "Success"})
571-
await bound_tool(message="hello")
572-
573-
# Verify the payload includes both the argument and the bound parameter
574-
expected_payload = {"message": "hello", "count": 100}
575-
m.assert_called_once_with(
576-
invoke_url, method="POST", json=expected_payload, headers={}
577-
)
578-
579-
580-
@pytest.mark.asyncio
581-
async def test_bind_params_success_with_callable(
582-
http_session: ClientSession,
583-
sample_tool_params: list[ParameterSchema],
584-
sample_tool_description: str,
585-
):
586-
"""
587-
Tests successfully binding multiple parameters, including one with a callable.
588-
"""
589-
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
590-
tool = ToolboxTool(
591-
transport=transport,
592-
name=TEST_TOOL_NAME,
593-
description=sample_tool_description,
594-
params=sample_tool_params,
595-
required_authn_params={},
596-
required_authz_tokens=[],
597-
auth_service_token_getters={},
598-
bound_params={},
599-
client_headers={},
600-
)
601-
602-
# Bind both parameters, one with a lambda
603-
bound_tool = tool.bind_params({"message": lambda: "from-callable", "count": 99})
604-
605-
assert "message" not in bound_tool.__signature__.parameters
606-
assert "count" not in bound_tool.__signature__.parameters
607-
assert len(bound_tool.__signature__.parameters) == 0
608-
609-
# Test invocation
610-
invoke_url = f"{HTTPS_BASE_URL}/api/tool/{TEST_TOOL_NAME}/invoke"
611-
with aioresponses() as m:
612-
m.post(invoke_url, status=200, payload={"result": "Success"})
613-
await bound_tool() # Call with no arguments
614-
615-
expected_payload = {"message": "from-callable", "count": 99}
616-
m.assert_called_once_with(
617-
invoke_url, method="POST", json=expected_payload, headers={}
618-
)
619-
620-
621-
def test_bind_param_invalid_parameter_name(toolbox_tool: ToolboxTool):
622-
"""
623-
Tests that binding a parameter that does not exist raises a ValueError.
624-
"""
625-
with pytest.raises(
626-
ValueError, match="unable to bind parameters: no parameter named invalid_param"
627-
):
628-
toolbox_tool.bind_param("invalid_param", "some_value")
629-
630-
631-
def test_bind_params_rebinding_parameter_fails(toolbox_tool: ToolboxTool):
632-
"""
633-
Tests that attempting to re-bind an already bound parameter raises a ValueError.
634-
"""
635-
tool_with_one_bound_param = toolbox_tool.bind_param("count", 50)
636-
637-
with pytest.raises(
638-
ValueError, match="cannot re-bind parameter: parameter 'count' is already bound"
639-
):
640-
tool_with_one_bound_param.bind_params({"count": 75})
641-
642-
643-
@pytest.mark.asyncio
644-
async def test_bind_param_chaining(
645-
http_session: ClientSession,
646-
sample_tool_params: list[ParameterSchema],
647-
sample_tool_description: str,
648-
):
649-
"""
650-
Tests that bind_param calls can be chained to bind multiple parameters sequentially.
651-
"""
652-
transport = ToolboxTransport(HTTPS_BASE_URL, http_session)
653-
tool = ToolboxTool(
654-
transport=transport,
655-
name=TEST_TOOL_NAME,
656-
description=sample_tool_description,
657-
params=sample_tool_params,
658-
required_authn_params={},
659-
required_authz_tokens=[],
660-
auth_service_token_getters={},
661-
bound_params={},
662-
client_headers={},
663-
)
664-
665-
# Chain the calls
666-
fully_bound_tool = tool.bind_param("count", 42).bind_param(
667-
"message", "chained-call"
668-
)
669-
670-
assert len(fully_bound_tool.__signature__.parameters) == 0
671-
assert fully_bound_tool._bound_params == {
672-
"count": 42,
673-
"message": "chained-call",
674-
}
675-
676-
# Test invocation
677-
invoke_url = f"{HTTPS_BASE_URL}/api/tool/{TEST_TOOL_NAME}/invoke"
678-
with aioresponses() as m:
679-
m.post(invoke_url, status=200, payload={"result": "Success"})
680-
await fully_bound_tool()
681-
682-
m.assert_called_once_with(
683-
invoke_url,
684-
method="POST",
685-
json={"count": 42, "message": "chained-call"},
686-
headers={},
687-
)

0 commit comments

Comments
 (0)