Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1de3c33
feat(core): add support for MCP 2026 stateless draft and auto-negotia…
anubhav756 Jun 26, 2026
b908993
fix: Keep the MCP_LATEST to stable version but point MCP_DRAFT to 202…
anubhav756 Jun 29, 2026
94c1556
chore: fix integration tests
anubhav756 Jun 29, 2026
fb1a630
chore: delint
anubhav756 Jun 29, 2026
0596a80
chore: fix tests
anubhav756 Jun 29, 2026
ff7ea75
test: increase integration test coverage
anubhav756 Jun 29, 2026
4647a5a
chore: delint
anubhav756 Jun 29, 2026
261bb86
test: integrate the draft build with dual testing modes
anubhav756 Jun 30, 2026
8b6ec1e
fix(tests): correct pytest async fixtures and auto-format with black/…
anubhav756 Jun 30, 2026
a25baf4
fix(tests): use pytest.fixture for sync fixtures and fix isort format…
anubhav756 Jun 30, 2026
66c8155
fix(tests): update binary URL for draft testing in all packages
anubhav756 Jun 30, 2026
28834dd
chore: fix integration tests url and lint errors
anubhav756 Jun 30, 2026
0c66983
chore: test with mcp-v202606 branch to fix unknown flag error
anubhav756 Jun 30, 2026
173e74f
fix(test): core fallback and adk toolset import
anubhav756 Jun 30, 2026
ee00b9e
chore: format files
anubhav756 Jun 30, 2026
b019d54
Revert "chore: format files"
anubhav756 Jun 30, 2026
adc0d07
chore(test): fix line length in bucket name selection
anubhav756 Jun 30, 2026
bb72dfb
chore: bump TOOLBOX_VERSION to v1.6.0
anubhav756 Jul 1, 2026
caf7b85
test: parameterize integration tests to run comprehensively for both …
anubhav756 Jul 1, 2026
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
2 changes: 1 addition & 1 deletion packages/toolbox-adk/integration.cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ options:
substitutions:
_VERSION: '3.13'
# Default values (can be overridden by triggers)
_TOOLBOX_VERSION: '1.4.0'
_TOOLBOX_VERSION: 'v1.6.0'
_TOOLBOX_MANIFEST_VERSION: '34'
1 change: 1 addition & 0 deletions packages/toolbox-adk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ test = [
"pytest==9.0.3",
"pytest-asyncio==1.4.0",
"pytest-cov==7.1.0",
"numpy<2.2.0",
"pytest-mock==3.15.1"
]

Expand Down
6 changes: 1 addition & 5 deletions packages/toolbox-adk/src/toolbox_adk/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@
from typing import Any, Awaitable, Callable, Dict, Mapping, Optional

import toolbox_core
from fastapi.openapi.models import (
OAuth2,
OAuthFlowAuthorizationCode,
OAuthFlows,
)
from fastapi.openapi.models import OAuth2, OAuthFlowAuthorizationCode, OAuthFlows
from google.adk.auth.auth_credential import (
AuthCredential,
AuthCredentialTypes,
Expand Down
76 changes: 59 additions & 17 deletions packages/toolbox-adk/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from typing import Generator

import google
import pytest_asyncio
import pytest
from google.auth import compute_engine
from google.cloud import secretmanager, storage

Expand Down Expand Up @@ -75,7 +75,8 @@ def get_toolbox_binary_url(toolbox_version: str) -> str:
arch = (
"arm64" if os_system == "darwin" and platform.machine() == "arm64" else "amd64"
)
return f"v{toolbox_version}/{os_system}/{arch}/toolbox"
ext = ".exe" if os_system == "windows" else ""
return f"{toolbox_version}/{os_system}/{arch}/toolbox{ext}"


def get_auth_token(client_id: str) -> str:
Expand All @@ -92,17 +93,17 @@ def get_auth_token(client_id: str) -> str:


#### Define Fixtures
@pytest_asyncio.fixture(scope="session")
@pytest.fixture(scope="session")
def project_id() -> str:
return get_env_var("GOOGLE_CLOUD_PROJECT")


@pytest_asyncio.fixture(scope="session")
@pytest.fixture(scope="session")
def toolbox_version() -> str:
return get_env_var("TOOLBOX_VERSION")


@pytest_asyncio.fixture(scope="session")
@pytest.fixture(scope="session")
def tools_file_path(project_id: str) -> Generator[str]:
"""Provides a temporary file path containing the tools manifest."""
if os.environ.get("TEST_MOCK_GCP"):
Expand All @@ -122,7 +123,7 @@ def tools_file_path(project_id: str) -> Generator[str]:
os.remove(tools_file_path)


@pytest_asyncio.fixture(scope="session")
@pytest.fixture(scope="session")
def auth_token1(project_id: str) -> str:
if os.environ.get("TEST_MOCK_GCP"):
return "mock-token-1"
Expand All @@ -132,7 +133,7 @@ def auth_token1(project_id: str) -> str:
return get_auth_token(client_id)


@pytest_asyncio.fixture(scope="session")
@pytest.fixture(scope="session")
def auth_token2(project_id: str) -> str:
if os.environ.get("TEST_MOCK_GCP"):
return "mock-token-2"
Expand All @@ -142,7 +143,7 @@ def auth_token2(project_id: str) -> str:
return get_auth_token(client_id)


@pytest_asyncio.fixture(scope="session")
@pytest.fixture(scope="session")
def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]:
"""Starts the toolbox server as a subprocess."""
if os.environ.get("TEST_MOCK_GCP"):
Expand All @@ -151,34 +152,75 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None

print("Downloading toolbox binary from gcs bucket...")
source_blob_name = get_toolbox_binary_url(toolbox_version)
download_blob("mcp-toolbox-for-databases", source_blob_name, "toolbox")
bucket_name = (
"mcp-toolbox-for-databases-dev"
if toolbox_version in ("main", "mcp-v202606")
else "mcp-toolbox-for-databases"
)
download_blob(bucket_name, source_blob_name, "toolbox")

print("Toolbox binary downloaded successfully.")
try:
print("Opening toolbox server process...")
# Make toolbox executable
os.chmod("toolbox", 0o700)
# Run toolbox binary
toolbox_server = subprocess.Popen(
["./toolbox", "--tools-file", tools_file_path]
toolbox_server_1 = subprocess.Popen(
["./toolbox", "--port", "5000", "--tools-file", tools_file_path]
)
toolbox_server_2 = subprocess.Popen(
[
"./toolbox",
"--port",
"5001",
"--tools-file",
tools_file_path,
"--enable-draft-specs",
]
)

# Wait for server to start
# Retry logic with a timeout
for _ in range(5): # retries
time.sleep(2)
print("Checking if toolbox is successfully started...")
if toolbox_server.poll() is None:
print("Toolbox server started successfully.")
print("Checking if both toolbox servers are successfully started...")
if toolbox_server_1.poll() is None and toolbox_server_2.poll() is None:
print("Toolbox servers started successfully.")
break
else:
raise RuntimeError("Toolbox server failed to start after 5 retries.")
raise RuntimeError("Toolbox servers failed to start after 5 retries.")
except subprocess.CalledProcessError as e:
print(e.stderr.decode("utf-8"))
print(e.stdout.decode("utf-8"))
raise RuntimeError(f"{e}\n\n{e.stderr.decode('utf-8')}") from e
yield

# Clean up toolbox server
toolbox_server.terminate()
toolbox_server.wait(timeout=5)
toolbox_server_1.terminate()
toolbox_server_2.terminate()
toolbox_server_1.wait(timeout=5)
toolbox_server_2.wait(timeout=5)


@pytest.fixture(
params=["http://localhost:5000", "http://localhost:5001"], scope="session"
)
def toolbox_server_url(request) -> str:
return request.param


@pytest.fixture(autouse=True)
def patch_toolbox_client_url(toolbox_server_url):
from toolbox_adk.toolset import ToolboxToolset

original_init = ToolboxToolset.__init__

def new_init(self, server_url="http://localhost:5000", *args, **kwargs):
if server_url == "http://localhost:5000":
server_url = toolbox_server_url
original_init(self, server_url, *args, **kwargs)

from unittest.mock import patch

with patch.object(ToolboxToolset, "__init__", new_init, create=True):
yield
25 changes: 5 additions & 20 deletions packages/toolbox-adk/tests/unit/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,7 @@ def test_from_adk_credentials_http_bearer(self):

def test_from_adk_credentials_api_key(self):
from fastapi.openapi.models import APIKey, APIKeyIn
from google.adk.auth.auth_credential import (
AuthCredential,
AuthCredentialTypes,
)
from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes

auth_credential = AuthCredential(
auth_type=AuthCredentialTypes.API_KEY, api_key="abc"
Expand All @@ -129,10 +126,7 @@ def test_from_adk_credentials_api_key(self):

def test_from_adk_credentials_api_key_default_location(self):
from fastapi.openapi.models import APIKey
from google.adk.auth.auth_credential import (
AuthCredential,
AuthCredentialTypes,
)
from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes

auth_credential = AuthCredential(
auth_type=AuthCredentialTypes.API_KEY, api_key="abc"
Expand All @@ -157,10 +151,7 @@ class MockScheme:
def test_from_adk_credentials_api_key_query_fail(self):
import pytest
from fastapi.openapi.models import APIKey, APIKeyIn
from google.adk.auth.auth_credential import (
AuthCredential,
AuthCredentialTypes,
)
from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes

cred = AuthCredential(auth_type=AuthCredentialTypes.API_KEY, api_key="abc")
scheme = APIKey(type="apiKey", name="key", **{"in": APIKeyIn.query})
Expand All @@ -172,10 +163,7 @@ def test_from_adk_credentials_api_key_query_fail(self):

def test_from_adk_credentials_api_key_no_scheme_raises(self):
import pytest
from google.adk.auth.auth_credential import (
AuthCredential,
AuthCredentialTypes,
)
from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes

auth_credential = AuthCredential(
auth_type=AuthCredentialTypes.API_KEY, api_key="my-key"
Expand All @@ -187,10 +175,7 @@ def test_from_adk_credentials_api_key_no_scheme_raises(self):

def test_from_adk_credentials_unsupported(self):
import pytest
from google.adk.auth.auth_credential import (
AuthCredential,
AuthCredentialTypes,
)
from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes

auth_credential = AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2
Expand Down
2 changes: 1 addition & 1 deletion packages/toolbox-core/integration.cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ options:
logging: CLOUD_LOGGING_ONLY
substitutions:
_VERSION: '3.13'
_TOOLBOX_VERSION: '1.4.0'
_TOOLBOX_VERSION: 'v1.6.0'
_TOOLBOX_MANIFEST_VERSION: '34'
1 change: 1 addition & 0 deletions packages/toolbox-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ test = [
"pytest-aioresponses==0.3.0",
"pytest-asyncio==1.4.0",
"pytest-cov==7.1.0",
"numpy<2.2.0",
"pytest-mock==3.15.1",
"google-cloud-secret-manager==2.28.0",
"google-cloud-storage==3.10.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/toolbox-core/src/toolbox_core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def __init__(

def _create_transport(self, protocol: Protocol) -> ITransport:
match protocol:
case Protocol.MCP_LATEST:
case Protocol.MCP_DRAFT:
return McpHttpTransportV20260618(
self._url,
self._session,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pydantic import BaseModel

from ... import version
from ...exceptions import ProtocolNegotiationError
from ...protocol import ManifestSchema, TelemetryAttributes
from .. import telemetry
from ..transport_base import _McpHttpTransportBase
Expand Down Expand Up @@ -131,9 +132,7 @@ async def _initialize_session(
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}"
)
raise ProtocolNegotiationError(result.protocolVersion)

if not result.capabilities.tools:
if self._manage_session:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pydantic import BaseModel

from ... import version
from ...exceptions import ProtocolNegotiationError
from ...protocol import ManifestSchema, TelemetryAttributes
from .. import telemetry
from ..transport_base import _McpHttpTransportBase
Expand Down Expand Up @@ -147,10 +148,7 @@ async def _initialize_session(
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}"
)
raise ProtocolNegotiationError(result.protocolVersion)

if not result.capabilities.tools:
if self._manage_session:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pydantic import BaseModel

from ... import version
from ...exceptions import ProtocolNegotiationError
from ...protocol import ManifestSchema, TelemetryAttributes
from .. import telemetry
from ..transport_base import _McpHttpTransportBase
Expand Down Expand Up @@ -138,10 +139,7 @@ async def _initialize_session(
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}"
)
raise ProtocolNegotiationError(result.protocolVersion)

if not result.capabilities.tools:
if self._manage_session:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pydantic import BaseModel

from ... import version
from ...exceptions import ProtocolNegotiationError
from ...protocol import ManifestSchema, TelemetryAttributes
from .. import telemetry
from ..transport_base import _McpHttpTransportBase
Expand Down Expand Up @@ -138,10 +139,7 @@ async def _initialize_session(
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}"
)
raise ProtocolNegotiationError(result.protocolVersion)

if not result.capabilities.tools:
if self._manage_session:
Expand Down
9 changes: 6 additions & 3 deletions packages/toolbox-core/src/toolbox_core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@ class Protocol(str, Enum):
MCP_v20250326 = "2025-03-26"
MCP_v20241105 = "2024-11-05"
MCP_v20251125 = "2025-11-25"
MCP = MCP_v20250618
MCP_LATEST = "DRAFT-2026-v1"
MCP_v2026_DRAFT = "DRAFT-2026-v1"

MCP = MCP_v20251125
MCP_LATEST = MCP_v20251125
MCP_DRAFT = MCP_v2026_DRAFT

@staticmethod
def get_supported_mcp_versions() -> list[str]:
"""Returns a list of supported MCP protocol versions."""
return [
Protocol.MCP_LATEST.value,
Protocol.MCP_DRAFT.value,
Protocol.MCP_v20251125.value,
Protocol.MCP_v20250618.value,
Protocol.MCP_v20250326.value,
Expand Down
Loading