Skip to content

Commit 145b7bf

Browse files
google-genai-botcopybara-github
authored andcommitted
chore: defer mcp SDK import to first MCP usage
PiperOrigin-RevId: 904039259
1 parent d22ea99 commit 145b7bf

5 files changed

Lines changed: 109 additions & 90 deletions

File tree

google/genai/_extra_utils.py

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,6 @@
4545
else:
4646
McpClientSession: typing.Type = Any
4747
McpTool: typing.Type = Any
48-
try:
49-
from mcp import ClientSession as McpClientSession
50-
from mcp.types import Tool as McpTool
51-
except ImportError:
52-
McpClientSession = None
53-
McpTool = None
5448

5549
_DEFAULT_MAX_REMOTE_CALLS_AFC = 10
5650

@@ -568,27 +562,32 @@ async def parse_config_for_mcp_sessions(
568562
parsed_config_copy = parsed_config.model_copy(update={'tools': None})
569563
if parsed_config.tools:
570564
parsed_config_copy.tools = []
571-
for tool in parsed_config.tools:
572-
if McpClientSession is not None and isinstance(tool, McpClientSession):
573-
mcp_to_genai_tool_adapter = McpToGenAiToolAdapter(
574-
tool, await tool.list_tools()
575-
)
576-
# Extend the config with the MCP session tools converted to GenAI tools.
577-
parsed_config_copy.tools.extend(mcp_to_genai_tool_adapter.tools)
578-
for genai_tool in mcp_to_genai_tool_adapter.tools:
579-
if genai_tool.function_declarations:
580-
for function_declaration in genai_tool.function_declarations:
581-
if function_declaration.name:
582-
if mcp_to_genai_tool_adapters.get(function_declaration.name):
583-
raise ValueError(
584-
f'Tool {function_declaration.name} is already defined for'
585-
' the request.'
565+
if 'mcp' not in sys.modules:
566+
# No MCP tools possible if `mcp` isn't loaded; pass through unchanged.
567+
parsed_config_copy.tools.extend(parsed_config.tools)
568+
else:
569+
from mcp import ClientSession as _McpClientSession
570+
for tool in parsed_config.tools:
571+
if isinstance(tool, _McpClientSession):
572+
mcp_to_genai_tool_adapter = McpToGenAiToolAdapter(
573+
tool, await tool.list_tools()
574+
)
575+
# Extend the config with the MCP session tools converted to GenAI tools.
576+
parsed_config_copy.tools.extend(mcp_to_genai_tool_adapter.tools)
577+
for genai_tool in mcp_to_genai_tool_adapter.tools:
578+
if genai_tool.function_declarations:
579+
for function_declaration in genai_tool.function_declarations:
580+
if function_declaration.name:
581+
if mcp_to_genai_tool_adapters.get(function_declaration.name):
582+
raise ValueError(
583+
f'Tool {function_declaration.name} is already defined for'
584+
' the request.'
585+
)
586+
mcp_to_genai_tool_adapters[function_declaration.name] = (
587+
mcp_to_genai_tool_adapter
586588
)
587-
mcp_to_genai_tool_adapters[function_declaration.name] = (
588-
mcp_to_genai_tool_adapter
589-
)
590-
else:
591-
parsed_config_copy.tools.append(tool)
589+
else:
590+
parsed_config_copy.tools.append(tool)
592591

593592
return parsed_config_copy, mcp_to_genai_tool_adapters
594593

google/genai/_mcp_utils.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""Utils for working with MCP tools."""
1717

1818
from importlib.metadata import PackageNotFoundError, version
19+
import sys
1920
import typing
2021
from typing import Any
2122

@@ -28,12 +29,16 @@
2829
else:
2930
McpClientSession: typing.Type = Any
3031
McpTool: typing.Type = Any
31-
try:
32-
from mcp.types import Tool as McpTool
33-
from mcp import ClientSession as McpClientSession
34-
except ImportError:
35-
McpTool = None
36-
McpClientSession = None
32+
33+
34+
def _is_mcp_loaded() -> bool:
35+
"""True iff `mcp` is already imported in this process.
36+
37+
An MCP tool/session can only exist if the user has already imported `mcp`,
38+
so we can gate isinstance checks behind this without ever importing `mcp`
39+
ourselves at module load.
40+
"""
41+
return "mcp" in sys.modules
3742

3843

3944
def mcp_to_gemini_tool(tool: McpTool) -> types.Tool:
@@ -58,27 +63,32 @@ def mcp_to_gemini_tools(tools: list[McpTool]) -> list[types.Tool]:
5863

5964
def has_mcp_tool_usage(tools: types.ToolListUnion) -> bool:
6065
"""Checks whether the list of tools contains any MCP tools or sessions."""
61-
if McpClientSession is None:
66+
if not _is_mcp_loaded():
6267
return False
68+
from mcp import ClientSession as _McpClientSession
69+
from mcp.types import Tool as _McpTool
70+
6371
for tool in tools:
64-
if isinstance(tool, McpTool) or isinstance(tool, McpClientSession):
72+
if isinstance(tool, _McpTool) or isinstance(tool, _McpClientSession):
6573
return True
6674
return False
6775

6876

6977
def has_mcp_session_usage(tools: types.ToolListUnion) -> bool:
7078
"""Checks whether the list of tools contains any MCP sessions."""
71-
if McpClientSession is None:
79+
if not _is_mcp_loaded():
7280
return False
81+
from mcp import ClientSession as _McpClientSession
82+
7383
for tool in tools:
74-
if isinstance(tool, McpClientSession):
84+
if isinstance(tool, _McpClientSession):
7585
return True
7686
return False
7787

7888

7989
def set_mcp_usage_header(headers: dict[str, str]) -> None:
8090
"""Sets the MCP version label in the Google API client header."""
81-
if McpClientSession is None:
91+
if not _is_mcp_loaded():
8292
return
8393
try:
8494
version_label = version("mcp")

google/genai/_transformers.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,6 @@
5757
else:
5858
McpClientSession: typing.Type = Any
5959
McpTool: typing.Type = Any
60-
try:
61-
from mcp import ClientSession as McpClientSession
62-
from mcp.types import Tool as McpTool
63-
except ImportError:
64-
McpClientSession = None
65-
McpTool = None
6660

6761

6862
metric_name_sdk_api_map = {
@@ -967,12 +961,14 @@ def t_tool(
967961
)
968962
]
969963
)
970-
elif McpTool is not None and is_duck_type_of(origin, McpTool):
971-
return mcp_to_gemini_tool(origin)
972-
elif isinstance(origin, dict):
964+
if 'mcp' in sys.modules:
965+
from mcp.types import Tool as _McpTool
966+
967+
if is_duck_type_of(origin, _McpTool):
968+
return mcp_to_gemini_tool(origin)
969+
if isinstance(origin, dict):
973970
return types.Tool.model_validate(origin)
974-
else:
975-
return origin
971+
return origin
976972

977973

978974
def t_tools(

google/genai/live.py

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import contextlib
2121
import json
2222
import logging
23+
import sys
2324
import typing
2425
from typing import Any, AsyncIterator, Optional, Sequence, Union, get_args
2526
import warnings
@@ -59,22 +60,12 @@
5960
if typing.TYPE_CHECKING:
6061
from mcp import ClientSession as McpClientSession
6162
from mcp.types import Tool as McpTool
62-
from ._adapters import McpToGenAiToolAdapter
63-
from ._mcp_utils import mcp_to_gemini_tool
6463
else:
6564
McpClientSession: typing.Type = Any
6665
McpTool: typing.Type = Any
67-
McpToGenAiToolAdapter: typing.Type = Any
68-
try:
69-
from mcp import ClientSession as McpClientSession
70-
from mcp.types import Tool as McpTool
71-
from ._adapters import McpToGenAiToolAdapter
72-
from ._mcp_utils import mcp_to_gemini_tool
73-
except ImportError:
74-
McpClientSession = None
75-
McpTool = None
76-
McpToGenAiToolAdapter = None
77-
mcp_to_gemini_tool = None
66+
67+
from ._adapters import McpToGenAiToolAdapter
68+
from ._mcp_utils import mcp_to_gemini_tool
7869

7970
logger = logging.getLogger('google_genai.live')
8071

@@ -1174,17 +1165,23 @@ async def _t_live_connect_config(
11741165
parameter_model_copy = parameter_model.model_copy(update={'tools': None})
11751166
if parameter_model.tools:
11761167
parameter_model_copy.tools = []
1177-
for tool in parameter_model.tools:
1178-
if McpClientSession is not None and isinstance(tool, McpClientSession):
1179-
mcp_to_genai_tool_adapter = McpToGenAiToolAdapter(
1180-
tool, await tool.list_tools()
1181-
)
1182-
# Extend the config with the MCP session tools converted to GenAI tools.
1183-
parameter_model_copy.tools.extend(mcp_to_genai_tool_adapter.tools)
1184-
elif McpTool is not None and isinstance(tool, McpTool):
1185-
parameter_model_copy.tools.append(mcp_to_gemini_tool(tool))
1186-
else:
1187-
parameter_model_copy.tools.append(tool)
1168+
if 'mcp' not in sys.modules:
1169+
# No MCP tools possible if `mcp` isn't loaded; pass through unchanged.
1170+
parameter_model_copy.tools.extend(parameter_model.tools)
1171+
else:
1172+
from mcp import ClientSession as _McpClientSession
1173+
from mcp.types import Tool as _McpTool
1174+
for tool in parameter_model.tools:
1175+
if isinstance(tool, _McpClientSession):
1176+
mcp_to_genai_tool_adapter = McpToGenAiToolAdapter(
1177+
tool, await tool.list_tools()
1178+
)
1179+
# Extend the config with the MCP session tools converted to GenAI tools.
1180+
parameter_model_copy.tools.extend(mcp_to_genai_tool_adapter.tools)
1181+
elif isinstance(tool, _McpTool):
1182+
parameter_model_copy.tools.append(mcp_to_gemini_tool(tool))
1183+
else:
1184+
parameter_model_copy.tools.append(tool)
11881185

11891186
if parameter_model_copy.generation_config is not None:
11901187
warnings.warn(

google/genai/types.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
import sys
2626
import types as builtin_types
2727
import typing
28-
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Union, _UnionGenericAlias # type: ignore
28+
from typing import Annotated, Any, Callable, Dict, List, Literal, Optional, Sequence, Union, _UnionGenericAlias # type: ignore
2929
import pydantic
30-
from pydantic import ConfigDict, Field, PrivateAttr, model_validator
30+
from pydantic import ConfigDict, Field, PrivateAttr, WrapValidator, model_validator
3131
from typing_extensions import Self, TypedDict
3232
from . import _common
3333
from ._operations_converters import (
@@ -63,25 +63,18 @@
6363
except ImportError:
6464
PIL_Image = None
6565

66-
_is_mcp_imported = False
6766
if typing.TYPE_CHECKING:
6867
from mcp import types as mcp_types
6968
from mcp import ClientSession as McpClientSession
7069
from mcp.types import CallToolResult as McpCallToolResult
71-
72-
_is_mcp_imported = True
7370
else:
7471
McpClientSession: typing.Type = Any
7572
McpCallToolResult: typing.Type = Any
76-
try:
77-
from mcp import types as mcp_types
78-
from mcp import ClientSession as McpClientSession
79-
from mcp.types import CallToolResult as McpCallToolResult
8073

81-
_is_mcp_imported = True
82-
except ImportError:
83-
McpClientSession = None
84-
McpCallToolResult = None
74+
75+
def _is_mcp_imported() -> bool:
76+
return 'mcp' in sys.modules
77+
8578

8679
if typing.TYPE_CHECKING:
8780
import yaml
@@ -1789,7 +1782,7 @@ class FunctionResponse(_common.BaseModel):
17891782
def from_mcp_response(
17901783
cls, *, name: str, response: McpCallToolResult
17911784
) -> 'FunctionResponse':
1792-
if not _is_mcp_imported:
1785+
if not _is_mcp_imported():
17931786
raise ValueError(
17941787
'MCP response is not supported. Please ensure that the MCP library is'
17951788
' imported.'
@@ -4888,17 +4881,41 @@ class ToolDict(TypedDict, total=False):
48884881

48894882

48904883
ToolOrDict = Union[Tool, ToolDict]
4891-
if _is_mcp_imported:
4884+
4885+
4886+
def _validate_tool_list(v: Any, handler: Any) -> Any:
4887+
"""Pass MCP tool/session objects through Pydantic validation untouched.
4888+
4889+
`Tool` has all-optional fields, so without this wrapper Pydantic's default
4890+
Union resolution would coerce any non-Tool item (e.g. an `mcp.ClientSession`)
4891+
into an empty `Tool()`. This wrapper dispatches dict -> Tool and leaves
4892+
every other type identity-preserved so MCP routing downstream still works.
4893+
"""
4894+
if v is None:
4895+
return None
4896+
if not isinstance(v, list):
4897+
return handler(v)
4898+
out = []
4899+
for item in v:
4900+
if isinstance(item, dict):
4901+
out.append(Tool.model_validate(item))
4902+
else:
4903+
out.append(item)
4904+
return out
4905+
4906+
4907+
if typing.TYPE_CHECKING:
48924908
ToolUnion = Union[Tool, Callable[..., Any], mcp_types.Tool, McpClientSession]
48934909
ToolUnionDict = Union[
48944910
ToolDict, Callable[..., Any], mcp_types.Tool, McpClientSession
48954911
]
4912+
ToolListUnion = list[ToolUnion]
4913+
ToolListUnionDict = list[ToolUnionDict]
48964914
else:
48974915
ToolUnion = Union[Tool, Callable[..., Any]] # type: ignore[misc]
48984916
ToolUnionDict = Union[ToolDict, Callable[..., Any]] # type: ignore[misc]
4899-
4900-
ToolListUnion = list[ToolUnion]
4901-
ToolListUnionDict = list[ToolUnionDict]
4917+
ToolListUnion = Annotated[list, WrapValidator(_validate_tool_list)]
4918+
ToolListUnionDict = list[ToolUnionDict]
49024919

49034920
SchemaUnion = Union[
49044921
dict[Any, Any], type, Schema, builtin_types.GenericAlias, VersionedUnionType # type: ignore[valid-type]

0 commit comments

Comments
 (0)