Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 12 additions & 70 deletions azure/functions/decorators/function_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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__
Expand Down
58 changes: 45 additions & 13 deletions azure/functions/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -200,19 +229,22 @@ 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)
for block in obj.content]
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)

Expand Down
11 changes: 10 additions & 1 deletion tests/decorators/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
Loading