Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
04f7962
add basic code
twishabansal Aug 25, 2025
9ba5bbc
fixes
twishabansal Aug 25, 2025
34cb50a
test fix
twishabansal Aug 25, 2025
ea417ba
new unit tests
twishabansal Aug 25, 2025
aa356b1
Merge branch 'main' into mcp-restructure
twishabansal Aug 25, 2025
7c0f917
rename ToolboxTransport
twishabansal Aug 25, 2025
434e91d
add py3.9 support
twishabansal Aug 25, 2025
31d0f9e
fix langchain tool tests
twishabansal Aug 25, 2025
b50513b
test fix
twishabansal Aug 25, 2025
c222d25
lint
twishabansal Aug 25, 2025
b244740
fix tests
twishabansal Aug 25, 2025
4b0f561
Merge branch 'main' into mcp-restructure
twishabansal Aug 28, 2025
d93f4dd
move manage session into transport
twishabansal Aug 28, 2025
f9d8e64
move warning to diff file
twishabansal Aug 28, 2025
d6361ed
avoid code duplication
twishabansal Aug 28, 2025
0f3bacc
fix tests
twishabansal Aug 28, 2025
0a79f5a
lint
twishabansal Aug 28, 2025
fcf7da3
remove redundant tests
twishabansal Aug 28, 2025
c65a94a
make invoke method return str
twishabansal Sep 1, 2025
e2269f8
lint
twishabansal Sep 1, 2025
25802e2
fix return type
twishabansal Sep 1, 2025
d41aed8
small refactor
twishabansal Sep 1, 2025
04263ff
refactor: remove transport logic from client tests
twishabansal Sep 2, 2025
47a2a46
try
twishabansal Aug 26, 2025
9cd9a79
version negotiation
twishabansal Aug 26, 2025
5964bcc
small changes
twishabansal Aug 26, 2025
4bac725
lint
twishabansal Aug 26, 2025
d8c6efb
fix endpoint
twishabansal Aug 26, 2025
b17e3ee
add some todos
twishabansal Aug 28, 2025
5b4d12c
lint
twishabansal Aug 28, 2025
388c7f9
initialise in init
twishabansal Aug 28, 2025
ef4e543
lint
twishabansal Aug 28, 2025
da384be
add support for 'Mcp-session-id'
twishabansal Aug 29, 2025
c2ad274
lint
twishabansal Aug 29, 2025
e88dfa7
add todo
twishabansal Aug 29, 2025
c9728a9
add mcp protocol version to the latest protocol
twishabansal Aug 29, 2025
c66dd26
add test coverage
twishabansal Aug 29, 2025
3cd00ea
small fix
twishabansal Aug 29, 2025
11ac6a2
small fix
twishabansal Aug 29, 2025
02baad7
small fix
twishabansal Aug 29, 2025
6ae38e1
thread fixes
twishabansal Aug 29, 2025
fb59bb5
try
twishabansal Aug 29, 2025
765db81
add tests
twishabansal Aug 29, 2025
f1c0807
lint
twishabansal Aug 29, 2025
24db78d
change small
twishabansal Aug 29, 2025
dcc811a
nit
twishabansal Sep 1, 2025
a4a4f55
small debugging
twishabansal Sep 1, 2025
19a1cf2
add todos
twishabansal Sep 1, 2025
914ec46
small bug fixes
twishabansal Sep 1, 2025
e922472
add todo
twishabansal Sep 1, 2025
8c14096
remove id field from notifications
twishabansal Sep 1, 2025
6c97083
refactor
twishabansal Sep 1, 2025
9dfa8cb
preprocess tools with empty params
twishabansal Sep 1, 2025
6f74838
fix types
twishabansal Sep 1, 2025
9118a89
fix bugs
twishabansal Sep 1, 2025
fbce7e9
better error log
twishabansal Sep 1, 2025
b6b2dbe
small cleanup
twishabansal Sep 1, 2025
ac2a924
handle notifications
twishabansal Sep 1, 2025
1fd0581
fix unit tests
twishabansal Sep 1, 2025
ec17eb8
lint
twishabansal Sep 1, 2025
1cffac1
decouple client from transport
twishabansal Sep 1, 2025
cc30a17
lint
twishabansal Sep 1, 2025
2f04c95
use toolbox protocol for e2e tests
twishabansal Sep 1, 2025
d80c41f
add e2e tests for mcp
twishabansal Sep 1, 2025
baf9d06
lint
twishabansal Sep 1, 2025
cd9841e
remove mcp as default protocol
twishabansal Sep 2, 2025
83030dc
remove auth tests from mcp
twishabansal Sep 2, 2025
bb8dc97
remove redundant lines
twishabansal Sep 2, 2025
8920538
remove redundant lines
twishabansal Sep 2, 2025
f70710f
lint
twishabansal Sep 2, 2025
80c688a
revert some changes
twishabansal Sep 2, 2025
4c42d33
initialise session in a better way
twishabansal Sep 2, 2025
9c119e8
small fix
twishabansal Sep 2, 2025
3a181ab
added more test cov
twishabansal Sep 4, 2025
67bbb24
lint
twishabansal Sep 4, 2025
31809a1
rename private method
twishabansal Sep 4, 2025
e281556
Made methods private
twishabansal Sep 4, 2025
e0a1337
lint
twishabansal Sep 4, 2025
85e5d29
rename base url
twishabansal Sep 4, 2025
ac2acfe
resolve comment
twishabansal Sep 4, 2025
d061f3e
better readability
twishabansal Sep 4, 2025
09866c4
Merge branch 'main' into mcp-restructure
twishabansal Sep 8, 2025
c7455a8
Merge branch 'main' into mcp-restructure
twishabansal Oct 9, 2025
f43909e
fix tests
twishabansal Oct 9, 2025
f07d4b3
lint
twishabansal Oct 9, 2025
b0a8e45
Merge branch 'mcp-restructure' into client-transport-decouple
twishabansal Oct 9, 2025
815efed
fix tests
twishabansal Oct 9, 2025
999cd3b
lint
twishabansal Oct 13, 2025
45d66d9
refactor mcp versions
twishabansal Oct 14, 2025
a444a4b
lint
twishabansal Oct 14, 2025
073e930
added test coverage
twishabansal Oct 14, 2025
1e2d09c
Merge branch 'client-transport-decouple' into mcp-transport-implement
twishabansal Oct 14, 2025
05e7cdc
Merge branch 'main' into mcp-transport-implement
twishabansal Nov 19, 2025
d916093
refactor mcp
twishabansal Nov 19, 2025
ef15144
lint
twishabansal Nov 19, 2025
672e5d9
improve cov
twishabansal Nov 19, 2025
769f4a3
lint
twishabansal Nov 19, 2025
b814bed
removed process id
twishabansal Nov 19, 2025
1dfb828
Update class name
twishabansal Nov 24, 2025
ae260d0
remove mcp latest
twishabansal Nov 24, 2025
a180458
rename mcp.py
twishabansal Nov 24, 2025
d29b3d5
have a single method for session init
twishabansal Nov 24, 2025
efc59fd
lint
twishabansal Nov 24, 2025
bc6da15
better type checks for v20241105
twishabansal Nov 24, 2025
fd0b9a5
Revert "better type checks for v20241105"
twishabansal Dec 4, 2025
7388c7b
update type checking
twishabansal Dec 8, 2025
6349293
lint
twishabansal Dec 8, 2025
1fd63a0
clean file
twishabansal Dec 8, 2025
064aaa7
refactor files
twishabansal Dec 8, 2025
c058b65
refactor all versions
twishabansal Dec 8, 2025
030be42
fix mypy errors
twishabansal Dec 8, 2025
63d8a5a
refactor properly
twishabansal Dec 8, 2025
332ff09
lint
twishabansal Dec 8, 2025
0943ed5
run mcp e2e tests on all versions
twishabansal Dec 9, 2025
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
23 changes: 21 additions & 2 deletions packages/toolbox-core/src/toolbox_core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
from deprecated import deprecated

from .itransport import ITransport
from .protocol import ToolSchema
from .mcp_transport import (
McpHttpTransportV20241105,
McpHttpTransportV20250326,
McpHttpTransportV20250618,
)
from .protocol import Protocol, ToolSchema
from .tool import ToolboxTool
from .toolbox_transport import ToolboxTransport
from .utils import identify_auth_requirements, resolve_value
Expand All @@ -44,6 +49,7 @@ def __init__(
client_headers: Optional[
Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]]
] = None,
protocol: Protocol = Protocol.TOOLBOX,
):
"""
Initializes the ToolboxClient.
Expand All @@ -54,8 +60,21 @@ def __init__(
If None (default), a new session is created internally. Note that
if a session is provided, its lifecycle (including closing)
should typically be managed externally.
client_headers: Headers to include in each request sent through this client.
client_headers: Headers to include in each request sent through this
client.
protocol: The communication protocol to use.
"""
if protocol == Protocol.TOOLBOX:
self.__transport = ToolboxTransport(url, session)
elif protocol in Protocol.get_supported_mcp_versions():
if protocol == Protocol.MCP_v20250618:
self.__transport = McpHttpTransportV20250618(url, session, protocol)
elif protocol == Protocol.MCP_v20250326:
self.__transport = McpHttpTransportV20250326(url, session, protocol)
elif protocol == Protocol.MCP_v20241105:
self.__transport = McpHttpTransportV20241105(url, session, protocol)
else:
raise ValueError(f"Unsupported MCP protocol version: {protocol}")

self.__transport = ToolboxTransport(url, session)
self.__client_headers = client_headers if client_headers is not None else {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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 .v20241105.mcp import McpHttpTransportV20241105
from .v20250326.mcp import McpHttpTransportV20250326
from .v20250618.mcp import McpHttpTransportV20250618

__all__ = [
"McpHttpTransportV20241105",
"McpHttpTransportV20250326",
"McpHttpTransportV20250618",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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.

import asyncio
from abc import ABC, abstractmethod
from typing import Optional

from aiohttp import ClientSession

from ..itransport import ITransport
from ..protocol import (
AdditionalPropertiesSchema,
ParameterSchema,
Protocol,
ToolSchema,
)


class _McpHttpTransportBase(ITransport, ABC):
"""Base transport for MCP protocols."""

def __init__(
self,
base_url: str,
session: Optional[ClientSession] = None,
protocol: Protocol = Protocol.MCP,
):
self._mcp_base_url = f"{base_url}/mcp/"
self._protocol_version = protocol.value
self._server_version: Optional[str] = None

self._manage_session = session is None
self._session = session or ClientSession()
self._init_lock = asyncio.Lock()
self._init_task: Optional[asyncio.Task] = None

async def _ensure_initialized(self):
"""Ensures the session is initialized before making requests."""
async with self._init_lock:
if self._init_task is None:
self._init_task = asyncio.create_task(self._initialize_session())
await self._init_task

@property
def base_url(self) -> str:
return self._mcp_base_url

def _convert_tool_schema(self, tool_data: dict) -> ToolSchema:
"""Converts a raw MCP tool dictionary into the Toolbox ToolSchema."""
parameters = []
input_schema = tool_data.get("inputSchema", {})
properties = input_schema.get("properties", {})
required = input_schema.get("required", [])

for name, schema in properties.items():
additional_props = schema.get("additionalProperties")
if isinstance(additional_props, dict):
additional_props = AdditionalPropertiesSchema(
type=additional_props["type"]
)
else:
additional_props = True
parameters.append(
ParameterSchema(
name=name,
type=schema["type"],
description=schema.get("description", ""),
required=name in required,
additionalProperties=additional_props,
)
)

return ToolSchema(
description=tool_data.get("description") or "", parameters=parameters
)

async def close(self):
async with self._init_lock:
if self._init_task:
try:
await self._init_task
except Exception:
# If initialization failed, we can still try to close.
pass
if self._manage_session and self._session and not self._session.closed:
await self._session.close()

@abstractmethod
async def _initialize_session(self):
"""Initializes the MCP session."""
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# 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 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 McpHttpTransportV20241105(_McpHttpTransportBase):
"""Transport for the MCP v2024-11-05 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."""
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=dict(headers or {})
) as response:
if not response.ok:
error_text = await response.text()
raise RuntimeError(
f"API request failed with status {response.status} "
f"({response.reason}). Server response: {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:
raise RuntimeError(f"MCP request failed: {json_resp.get('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):
"""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)
)

self._server_version = result.serverInfo.version
if result.protocolVersion != self._protocol_version:
raise RuntimeError(
f"MCP version mismatch: client does not support server version {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()
)

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()

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()

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"
Loading