|
25 | 25 | import sys |
26 | 26 | import types as builtin_types |
27 | 27 | 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 |
29 | 29 | import pydantic |
30 | | -from pydantic import ConfigDict, Field, PrivateAttr, model_validator |
| 30 | +from pydantic import ConfigDict, Field, PrivateAttr, WrapValidator, model_validator |
31 | 31 | from typing_extensions import Self, TypedDict |
32 | 32 | from . import _common |
33 | 33 | from ._operations_converters import ( |
|
63 | 63 | except ImportError: |
64 | 64 | PIL_Image = None |
65 | 65 |
|
66 | | -_is_mcp_imported = False |
67 | 66 | if typing.TYPE_CHECKING: |
68 | 67 | from mcp import types as mcp_types |
69 | 68 | from mcp import ClientSession as McpClientSession |
70 | 69 | from mcp.types import CallToolResult as McpCallToolResult |
71 | 70 |
|
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': |
77 | 96 | from mcp import types as mcp_types |
78 | | - from mcp import ClientSession as McpClientSession |
79 | | - from mcp.types import CallToolResult as McpCallToolResult |
80 | 97 |
|
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 | + |
85 | 102 |
|
86 | 103 | if typing.TYPE_CHECKING: |
87 | 104 | import yaml |
@@ -1798,9 +1815,9 @@ class FunctionResponse(_common.BaseModel): |
1798 | 1815 |
|
1799 | 1816 | @classmethod |
1800 | 1817 | def from_mcp_response( |
1801 | | - cls, *, name: str, response: McpCallToolResult |
| 1818 | + cls, *, name: str, response: 'McpCallToolResult' |
1802 | 1819 | ) -> 'FunctionResponse': |
1803 | | - if not _is_mcp_imported: |
| 1820 | + if not _is_mcp_imported(): |
1804 | 1821 | raise ValueError( |
1805 | 1822 | 'MCP response is not supported. Please ensure that the MCP library is' |
1806 | 1823 | ' imported.' |
@@ -4907,17 +4924,41 @@ class ToolDict(TypedDict, total=False): |
4907 | 4924 |
|
4908 | 4925 |
|
4909 | 4926 | 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: |
4911 | 4951 | ToolUnion = Union[Tool, Callable[..., Any], mcp_types.Tool, McpClientSession] |
4912 | 4952 | ToolUnionDict = Union[ |
4913 | 4953 | ToolDict, Callable[..., Any], mcp_types.Tool, McpClientSession |
4914 | 4954 | ] |
| 4955 | + ToolListUnion = list[ToolUnion] |
| 4956 | + ToolListUnionDict = list[ToolUnionDict] |
4915 | 4957 | else: |
4916 | 4958 | ToolUnion = Union[Tool, Callable[..., Any]] # type: ignore[misc] |
4917 | 4959 | 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] |
4921 | 4962 |
|
4922 | 4963 | SchemaUnion = Union[ |
4923 | 4964 | dict[Any, Any], type, Schema, builtin_types.GenericAlias, VersionedUnionType # type: ignore[valid-type] |
|
0 commit comments