From 1a45f08d3f6026b81afd47cc1824da02573d6b4e Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Fri, 31 Jul 2026 16:10:25 -0500 Subject: [PATCH] Add support for MCP 2 re-exported types --- azure/functions/decorators/function_app.py | 82 ++++------------------ azure/functions/mcp.py | 58 +++++++++++---- tests/decorators/test_mcp.py | 11 ++- 3 files changed, 67 insertions(+), 84 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 3dd33e76..fbe241b4 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -56,7 +56,8 @@ from .retry_policy import RetryPolicy from .function_name import FunctionName from .warmup import WarmUpTrigger -from ..mcp import MCPToolContext, _is_mcp_call_tool_result, _is_mcp_sdk_type +from ..mcp import (MCPToolContext, _is_mcp_call_tool_result, + _is_mcp_sdk_type, _is_mcp_sdk_type_annotation) from .._http_asgi import AsgiMiddleware from .._http_wsgi import WsgiMiddleware, Context from azure.functions.decorators.mysql import MySqlInput, MySqlOutput, \ @@ -1684,16 +1685,8 @@ def decorator(fb: FunctionBuilder) -> FunctionBuilder: is_mcp_content = False is_mcp_sdk_type = False - # Try to detect official MCP SDK types by checking module - try: - if isinstance(return_annotation, type): - # Check if the type is from the mcp.types module - if hasattr(return_annotation, '__module__'): - module = return_annotation.__module__ - if module and (module.startswith('mcp.types') or module == 'mcp.types'): - is_mcp_sdk_type = True - except (ImportError, TypeError, AttributeError): - pass + is_mcp_sdk_type = _is_mcp_sdk_type_annotation( + return_annotation) # Check for @mcp_content decorated classes try: @@ -1709,46 +1702,6 @@ def decorator(fb: FunctionBuilder) -> FunctionBuilder: origin = typing.get_origin(return_annotation) args = typing.get_args(return_annotation) - # Check for official MCP SDK types in lists - if origin in (list, List): - # For List[T], check if T is an MCP type - try: - if len(args) > 0: - list_item_type = args[0] - # Check if it's a direct MCP type - if isinstance(list_item_type, type): - if hasattr(list_item_type, '__module__'): - module = list_item_type.__module__ - if (module - and (module.startswith('mcp.types') - or module == 'mcp.types')): - is_mcp_sdk_type = True - # Check if it's a Union of MCP types - elif hasattr(list_item_type, '__origin__'): - union_origin = typing.get_origin( - list_item_type) - if union_origin is Union: - union_args = typing.get_args( - list_item_type) - for union_arg in union_args: - if (isinstance(union_arg, type) - and union_arg is not # noqa - type(None)): - if hasattr(union_arg, - '__module__'): - module = ( - union_arg.__module__) - if (module - and (module.startswith( - 'mcp.types') - or module - == 'mcp.types')): - is_mcp_sdk_type = True - break - except (ImportError, TypeError, AttributeError, - IndexError): - pass - # Check for Optional[T] where T is an MCP type if origin is Union: for arg in args: @@ -1762,20 +1715,6 @@ def decorator(fb: FunctionBuilder) -> FunctionBuilder: except TypeError: pass - # Check for MCP SDK types in Union - try: - if isinstance(arg, type): - # Check module for mcp.types - if hasattr(arg, '__module__'): - module = arg.__module__ - if (module - and (module.startswith('mcp.types') - or module == 'mcp.types')): - is_mcp_sdk_type = True - break - except (ImportError, TypeError, AttributeError): - pass - # Auto-enable use_result_schema for MCP types if is_mcp_content or is_mcp_sdk_type: auto_use_result_schema = True @@ -1840,15 +1779,17 @@ async def wrapper(context: str, *args, **kwargs): # Serialize using model_dump() for Pydantic models if hasattr(result, 'model_dump'): result_dict = result.model_dump( - mode='json', exclude_none=True) + mode='json', exclude_none=True, by_alias=True) elif hasattr(result, 'dict'): - result_dict = result.dict(exclude_none=True) + result_dict = result.dict( + exclude_none=True, by_alias=True) else: # Fallback: convert to dict manually result_dict = { 'content': [ block.model_dump( - mode='json', exclude_none=True) + mode='json', exclude_none=True, + by_alias=True) if hasattr(block, 'model_dump') else dict(block) for block in result.content @@ -1907,10 +1848,11 @@ async def wrapper(context: str, *args, **kwargs): elif _is_mcp_sdk_type(result): if hasattr(result, 'model_dump'): result_dict = result.model_dump( - mode='json', exclude_none=True) + mode='json', exclude_none=True, by_alias=True) result_json = json.dumps(result_dict) elif hasattr(result, 'dict'): - result_dict = result.dict(exclude_none=True) + result_dict = result.dict( + exclude_none=True, by_alias=True) result_json = json.dumps(result_dict) else: result_dict = result.__dict__ diff --git a/azure/functions/mcp.py b/azure/functions/mcp.py index 10d0761b..00f327cd 100644 --- a/azure/functions/mcp.py +++ b/azure/functions/mcp.py @@ -36,14 +36,42 @@ def _is_mcp_sdk_type(obj: Any) -> bool: _mcp_types = None return False - # Check if the object's class is from the mcp.types module - obj_type = type(obj) - if hasattr(obj_type, '__module__'): - module = obj_type.__module__ - # Check if it's from mcp.types or any mcp submodule - if module and (module.startswith('mcp.types') or module == 'mcp.types'): + return _is_mcp_sdk_type_annotation(type(obj)) + + +def _is_mcp_sdk_type_annotation(annotation: Any) -> bool: + """Check if a return annotation is an official MCP SDK type. + + MCP SDK 1.x types are defined in ``mcp.types``. MCP SDK 2.x re-exports + types defined in ``mcp_types`` from that same public module. + """ + if isinstance(annotation, type): + module = getattr(annotation, '__module__', None) + if module and module.startswith('mcp.types'): return True + if module and (module == 'mcp_types' + or module.startswith('mcp_types.')): + global _MCP_SDK_AVAILABLE, _mcp_types + if not _MCP_SDK_AVAILABLE or _mcp_types is None: + try: + from mcp import types as _mcp_types + _MCP_SDK_AVAILABLE = True + except ImportError: + _mcp_types = None + return False + + return any(exported is annotation + for exported in vars(_mcp_types).values()) + + return False + + origin = typing.get_origin(annotation) + if origin in (list, typing.Union): + return any(arg is not type(None) + and _is_mcp_sdk_type_annotation(arg) + for arg in typing.get_args(annotation)) + return False @@ -74,9 +102,10 @@ def _serialize_content_block(block: Any) -> dict: if _is_mcp_sdk_type(block): # MCP SDK types should be JSON-serializable if hasattr(block, 'model_dump'): - return block.model_dump(mode='json', exclude_none=True) + return block.model_dump( + mode='json', exclude_none=True, by_alias=True) elif hasattr(block, 'dict'): - return block.dict(exclude_none=True) + return block.dict(exclude_none=True, by_alias=True) # If it's a dataclass (e.g., @mcp_content decorated) if is_dataclass(block) and not isinstance(block, type): @@ -200,9 +229,10 @@ def encode(cls, obj: typing.Any, *, elif _is_mcp_call_tool_result(obj): # Serialize the MCP SDK's CallToolResult if hasattr(obj, 'model_dump'): - result_dict = obj.model_dump(mode='json', exclude_none=True) + result_dict = obj.model_dump( + mode='json', exclude_none=True, by_alias=True) elif hasattr(obj, 'dict'): - result_dict = obj.dict(exclude_none=True) + result_dict = obj.dict(exclude_none=True, by_alias=True) else: # Fallback: try to access attributes directly content_blocks = [_serialize_content_block(block) @@ -210,9 +240,11 @@ def encode(cls, obj: typing.Any, *, result_dict = { 'content': content_blocks if hasattr(obj, 'content') else [], } - if (hasattr(obj, 'structured_content') - and obj.structured_content is not None): - result_dict['structuredContent'] = obj.structured_content + structured_content = getattr( + obj, 'structuredContent', + getattr(obj, 'structured_content', None)) + if structured_content is not None: + result_dict['structuredContent'] = structured_content result_json = json.dumps(result_dict) return meta.Datum(type='string', value=result_json) diff --git a/tests/decorators/test_mcp.py b/tests/decorators/test_mcp.py index 5a036221..7a07e44d 100644 --- a/tests/decorators/test_mcp.py +++ b/tests/decorators/test_mcp.py @@ -16,7 +16,8 @@ MCPResourceTrigger, MCPPromptTrigger) from azure.functions.mcp import (_MCPToolTriggerConverter, - MCPResourceTriggerConverter) + MCPResourceTriggerConverter, + _is_mcp_sdk_type) from azure.functions.meta import Datum from mcp.types import ( ResourceLink, @@ -27,6 +28,14 @@ class TestMCP(unittest.TestCase): + def test_legacy_mcp_type_module_remains_supported(self): + class LegacyMCPType: + pass + + LegacyMCPType.__module__ = "mcp.types" + + self.assertTrue(_is_mcp_sdk_type(LegacyMCPType())) + def test_mcp_tool_trigger_valid_creation(self): trigger = _MCPToolTrigger( name="context",