Skip to content

Commit 21a1b14

Browse files
google-genai-botcopybara-github
authored andcommitted
chore: defer mcp SDK import to first MCP usage
`import google.genai` no longer eagerly imports `mcp`. The public symbols `McpClientSession`, `McpCallToolResult`, and `mcp_types` resolve lazily on first attribute access (PEP 562 `__getattr__`). PiperOrigin-RevId: 905291907
1 parent ab5e328 commit 21a1b14

5 files changed

Lines changed: 137 additions & 93 deletions

File tree

google/genai/_extra_utils.py

Lines changed: 26 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,33 @@ 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+
571+
for tool in parsed_config.tools:
572+
if 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'
585+
' for the request.'
586+
)
587+
mcp_to_genai_tool_adapters[function_declaration.name] = (
588+
mcp_to_genai_tool_adapter
586589
)
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)
590+
else:
591+
parsed_config_copy.tools.append(tool)
592592

593593
return parsed_config_copy, mcp_to_genai_tool_adapters
594594

google/genai/_mcp_utils.py

Lines changed: 21 additions & 12 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:
@@ -78,27 +83,32 @@ def mcp_to_gemini_tools(
7883

7984
def has_mcp_tool_usage(tools: types.ToolListUnion) -> bool:
8085
"""Checks whether the list of tools contains any MCP tools or sessions."""
81-
if McpClientSession is None:
86+
if not _is_mcp_loaded():
8287
return False
88+
from mcp import ClientSession as _McpClientSession
89+
from mcp.types import Tool as _McpTool
90+
8391
for tool in tools:
84-
if isinstance(tool, McpTool) or isinstance(tool, McpClientSession):
92+
if isinstance(tool, _McpTool) or isinstance(tool, _McpClientSession):
8593
return True
8694
return False
8795

8896

8997
def has_mcp_session_usage(tools: types.ToolListUnion) -> bool:
9098
"""Checks whether the list of tools contains any MCP sessions."""
91-
if McpClientSession is None:
99+
if not _is_mcp_loaded():
92100
return False
101+
from mcp import ClientSession as _McpClientSession
102+
93103
for tool in tools:
94-
if isinstance(tool, McpClientSession):
104+
if isinstance(tool, _McpClientSession):
95105
return True
96106
return False
97107

98108

99109
def set_mcp_usage_header(headers: dict[str, str]) -> None:
100110
"""Sets the MCP version label in the Google API client header."""
101-
if McpClientSession is None:
111+
if not _is_mcp_loaded():
102112
return
103113
try:
104114
version_label = version("mcp")
@@ -145,4 +155,3 @@ def _filter_to_supported_schema(
145155
filtered_schema[field_name] = field_value
146156

147157
return filtered_schema
148-

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: 22 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,24 @@ 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+
1175+
for tool in parameter_model.tools:
1176+
if isinstance(tool, _McpClientSession):
1177+
mcp_to_genai_tool_adapter = McpToGenAiToolAdapter(
1178+
tool, await tool.list_tools()
1179+
)
1180+
# Extend the config with the MCP session tools converted to GenAI tools.
1181+
parameter_model_copy.tools.extend(mcp_to_genai_tool_adapter.tools)
1182+
elif isinstance(tool, _McpTool):
1183+
parameter_model_copy.tools.append(mcp_to_gemini_tool(tool))
1184+
else:
1185+
parameter_model_copy.tools.append(tool)
11881186

11891187
if parameter_model_copy.generation_config is not None:
11901188
warnings.warn(

google/genai/types.py

Lines changed: 61 additions & 20 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,42 @@
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
7170

72-
_is_mcp_imported = True
73-
else:
74-
McpClientSession: typing.Type = Any
75-
McpCallToolResult: typing.Type = Any
76-
try:
71+
72+
def _is_mcp_imported() -> bool:
73+
return 'mcp' in sys.modules
74+
75+
76+
# PEP 562: lazy-resolve mcp symbols on first attribute access so `import
77+
# google.genai` doesn't import `mcp` for users who don't use MCP.
78+
def __getattr__(name: str) -> Any:
79+
if name == 'McpClientSession':
80+
try:
81+
from mcp import ClientSession
82+
except ImportError:
83+
globals()[name] = None
84+
return None
85+
globals()[name] = ClientSession
86+
return ClientSession
87+
if name == 'McpCallToolResult':
88+
try:
89+
from mcp.types import CallToolResult
90+
except ImportError:
91+
globals()[name] = None
92+
return None
93+
globals()[name] = CallToolResult
94+
return CallToolResult
95+
if name == 'mcp_types':
7796
from mcp import types as mcp_types
78-
from mcp import ClientSession as McpClientSession
79-
from mcp.types import CallToolResult as McpCallToolResult
8097

81-
_is_mcp_imported = True
82-
except ImportError:
83-
McpClientSession = None
84-
McpCallToolResult = None
98+
globals()[name] = mcp_types
99+
return mcp_types
100+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
101+
85102

86103
if typing.TYPE_CHECKING:
87104
import yaml
@@ -1798,9 +1815,9 @@ class FunctionResponse(_common.BaseModel):
17981815

17991816
@classmethod
18001817
def from_mcp_response(
1801-
cls, *, name: str, response: McpCallToolResult
1818+
cls, *, name: str, response: 'McpCallToolResult'
18021819
) -> 'FunctionResponse':
1803-
if not _is_mcp_imported:
1820+
if not _is_mcp_imported():
18041821
raise ValueError(
18051822
'MCP response is not supported. Please ensure that the MCP library is'
18061823
' imported.'
@@ -4907,17 +4924,41 @@ class ToolDict(TypedDict, total=False):
49074924

49084925

49094926
ToolOrDict = Union[Tool, ToolDict]
4910-
if _is_mcp_imported:
4927+
4928+
4929+
def _validate_tool_list(v: Any, handler: Any) -> Any:
4930+
"""Pass MCP tool/session objects through Pydantic validation untouched.
4931+
4932+
`Tool` has all-optional fields, so without this wrapper Pydantic's default
4933+
Union resolution would coerce any non-Tool item (e.g. an `mcp.ClientSession`)
4934+
into an empty `Tool()`. This wrapper dispatches dict -> Tool and leaves
4935+
every other type identity-preserved so MCP routing downstream still works.
4936+
"""
4937+
if v is None:
4938+
return None
4939+
if not isinstance(v, list):
4940+
return handler(v)
4941+
out = []
4942+
for item in v:
4943+
if isinstance(item, dict):
4944+
out.append(Tool.model_validate(item))
4945+
else:
4946+
out.append(item)
4947+
return out
4948+
4949+
4950+
if typing.TYPE_CHECKING:
49114951
ToolUnion = Union[Tool, Callable[..., Any], mcp_types.Tool, McpClientSession]
49124952
ToolUnionDict = Union[
49134953
ToolDict, Callable[..., Any], mcp_types.Tool, McpClientSession
49144954
]
4955+
ToolListUnion = list[ToolUnion]
4956+
ToolListUnionDict = list[ToolUnionDict]
49154957
else:
49164958
ToolUnion = Union[Tool, Callable[..., Any]] # type: ignore[misc]
49174959
ToolUnionDict = Union[ToolDict, Callable[..., Any]] # type: ignore[misc]
4918-
4919-
ToolListUnion = list[ToolUnion]
4920-
ToolListUnionDict = list[ToolUnionDict]
4960+
ToolListUnion = Annotated[list, WrapValidator(_validate_tool_list)]
4961+
ToolListUnionDict = list[ToolUnionDict]
49214962

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

0 commit comments

Comments
 (0)