Skip to content

Commit 8a2cba0

Browse files
fix: support List[<MCP types>] return values for mcp_tool (#337)
* fix List[] return values * lint --------- Co-authored-by: Gavin Aguiar <80794152+gavin-aguiar@users.noreply.github.com>
1 parent f637b68 commit 8a2cba0

2 files changed

Lines changed: 104 additions & 10 deletions

File tree

azure/functions/decorators/function_app.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,16 +1663,44 @@ def decorator(fb: FunctionBuilder) -> FunctionBuilder:
16631663
args = typing.get_args(return_annotation)
16641664

16651665
# Check for official MCP SDK types in lists
1666-
try:
1667-
if isinstance(args[0], type):
1668-
# Check if the type is from the mcp.types module
1669-
if hasattr(args[0], '__module__'):
1670-
module = args[0].__module__
1671-
if module and (module.startswith('mcp.types')
1672-
or module == 'mcp.types'):
1673-
is_mcp_sdk_type = True
1674-
except (ImportError, TypeError, AttributeError):
1675-
pass
1666+
if origin in (list, List):
1667+
# For List[T], check if T is an MCP type
1668+
try:
1669+
if len(args) > 0:
1670+
list_item_type = args[0]
1671+
# Check if it's a direct MCP type
1672+
if isinstance(list_item_type, type):
1673+
if hasattr(list_item_type, '__module__'):
1674+
module = list_item_type.__module__
1675+
if (module
1676+
and (module.startswith('mcp.types')
1677+
or module == 'mcp.types')):
1678+
is_mcp_sdk_type = True
1679+
# Check if it's a Union of MCP types
1680+
elif hasattr(list_item_type, '__origin__'):
1681+
union_origin = typing.get_origin(
1682+
list_item_type)
1683+
if union_origin is Union:
1684+
union_args = typing.get_args(
1685+
list_item_type)
1686+
for union_arg in union_args:
1687+
if (isinstance(union_arg, type)
1688+
and union_arg is not
1689+
type(None)):
1690+
if hasattr(union_arg,
1691+
'__module__'):
1692+
module = (
1693+
union_arg.__module__)
1694+
if (module
1695+
and (module.startswith(
1696+
'mcp.types')
1697+
or module
1698+
== 'mcp.types')):
1699+
is_mcp_sdk_type = True
1700+
break
1701+
except (ImportError, TypeError, AttributeError,
1702+
IndexError):
1703+
pass
16761704

16771705
# Check for Optional[T] where T is an MCP type
16781706
if origin is Union:
@@ -1801,6 +1829,32 @@ async def wrapper(context: str, *args, **kwargs):
18011829
"structuredContent": structured_content_json
18021830
}))
18031831

1832+
# Handle lists of MCP SDK content blocks
1833+
# Wrap them in a CallToolResult structure
1834+
elif isinstance(result, list) and len(result) > 0:
1835+
first_item = result[0]
1836+
if _is_mcp_sdk_type(first_item):
1837+
# Serialize all blocks in the list
1838+
from ..mcp import _serialize_content_block
1839+
blocks_list = [_serialize_content_block(block)
1840+
for block in result]
1841+
1842+
# Create a CallToolResult-like structure
1843+
# containing the blocks
1844+
call_tool_result = {
1845+
"content": blocks_list
1846+
}
1847+
full_result_json = json.dumps(call_tool_result)
1848+
1849+
# Return in CallToolResult format
1850+
# (list of blocks doesn't have separate
1851+
# structuredContent)
1852+
return str(json.dumps({
1853+
"type": "call_tool_result",
1854+
"content": full_result_json,
1855+
"structuredContent": None
1856+
}))
1857+
18041858
# Handle all other MCP SDK types
18051859
# (TextContent, ImageContent, ResourceLink, etc.)
18061860
elif _is_mcp_sdk_type(result):

tests/decorators/test_mcp.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,18 @@ def get_texts() -> List[TextContent]:
675675
trigger = get_texts._function._bindings[0]
676676
self.assertTrue(trigger.use_result_schema)
677677

678+
def test_auto_detect_list_union_mcp_types(self):
679+
"""Test auto-detection of List[Union[MCP types]] return type"""
680+
from typing import Union
681+
682+
@self.app.mcp_tool()
683+
def get_mixed_content() -> List[Union[TextContent, ImageContent]]:
684+
"""Returns mixed content blocks"""
685+
return [TextContent(type="text", text="test")]
686+
687+
trigger = get_mixed_content._function._bindings[0]
688+
self.assertTrue(trigger.use_result_schema)
689+
678690
def test_auto_detect_optional_mcp_image_content(self):
679691
"""Test auto-detection of Optional[ImageContent] return type"""
680692
@self.app.mcp_tool()
@@ -907,6 +919,34 @@ def test_func() -> str:
907919
self.assertIn("content", result_obj)
908920
self.assertIn("structuredContent", result_obj)
909921

922+
def test_structured_content_in_list_of_mcp_types(self):
923+
"""Test that List[MCP SDK types] includes structuredContent"""
924+
@self.app.mcp_tool()
925+
def test_func() -> List[TextContent]:
926+
"""Test function"""
927+
return [
928+
TextContent(type="text", text="First item"),
929+
TextContent(type="text", text="Second item")
930+
]
931+
932+
wrapper = test_func._function._func
933+
context = json.dumps({"arguments": {}})
934+
result = asyncio.run(wrapper(context))
935+
result_obj = json.loads(result)
936+
937+
# List of content blocks is wrapped as CallToolResult
938+
self.assertIn("type", result_obj)
939+
self.assertEqual(result_obj["type"], "call_tool_result")
940+
self.assertIn("content", result_obj)
941+
self.assertIn("structuredContent", result_obj)
942+
943+
# Content contains the CallToolResult structure with the blocks
944+
content_obj = json.loads(result_obj["content"])
945+
self.assertIn("content", content_obj)
946+
self.assertEqual(len(content_obj["content"]), 2)
947+
self.assertEqual(content_obj["content"][0]["text"], "First item")
948+
self.assertEqual(content_obj["content"][1]["text"], "Second item")
949+
910950

911951
class TestMCPPackageNotInstalled(unittest.TestCase):
912952
"""Tests for graceful degradation when mcp package is not installed"""

0 commit comments

Comments
 (0)