From 55549211fbdf7921c2b748d68ca482a223236a8f Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Tue, 19 Aug 2025 12:03:34 -0500 Subject: [PATCH 1/4] Adding MCP decorators --- azure/functions/__init__.py | 5 + azure/functions/decorators/constants.py | 3 + azure/functions/decorators/function_app.py | 154 +++++++++++++++++++++ azure/functions/decorators/mcp.py | 64 +++++++++ azure/functions/mcp.py | 67 +++++++++ tests/decorators/test_mcp.py | 76 ++++++++++ 6 files changed, 369 insertions(+) create mode 100644 azure/functions/decorators/mcp.py create mode 100644 azure/functions/mcp.py create mode 100644 tests/decorators/test_mcp.py diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index ee2a01f1..ccf99408 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -39,6 +39,10 @@ from . import sql # NoQA from . import warmup # NoQA from . import mysql # NoQA +from . import mcp # NoQA +from .decorators.mcp import MCPToolTrigger, MCPToolInput, MCPToolOutput # NoQA +from .mcp import MCPToolRequest, MCPToolTriggerConverter # NoQA +from .mcp import MCPToolRequest, MCPToolTriggerConverter, MCPToolInputConverter, MCPToolOutputConverter # NoQA __all__ = ( @@ -71,6 +75,7 @@ 'WarmUpContext', 'MySqlRow', 'MySqlRowList', + 'MCPToolRequest', # Middlewares 'WsgiMiddleware', diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index f1e45257..b48fe2aa 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -45,3 +45,6 @@ SEMANTIC_SEARCH = "semanticSearch" MYSQL = "mysql" MYSQL_TRIGGER = "mysqlTrigger" +MCP_TOOL_TRIGGER = "mcpToolTrigger" +MCP_TOOL_INPUT = "mcpToolInput" +MCP_TOOL_OUTPUT = "mcpToolOutput" diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index a6fa5ca1..034f7084 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -42,6 +42,7 @@ AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, \ semantic_search_system_prompt, \ SemanticSearchInput, EmbeddingsStoreOutput +from .mcp import MCPToolTrigger, MCPToolInput, MCPToolOutput from .retry_policy import RetryPolicy from .function_name import FunctionName from .warmup import WarmUpTrigger @@ -1511,6 +1512,57 @@ def decorator(): return wrap + def mcp_tool_trigger(self, + arg_name: str, + tool_name: str, + description: Optional[str] = None, + tool_properties: Optional[str] = None, + data_type: Optional[Union[DataType, str]] = None, + **kwargs) -> Callable[..., Any]: + """ + The `mcp_tool_trigger` decorator adds :class:`MCPToolTrigger` to the + :class:`FunctionBuilder` object for building a :class:`Function` object + used in the worker function indexing model. + + This is equivalent to defining `MCPToolTrigger` in the `function.json`, + which enables the function to be triggered when MCP tool requests are + received by the host. + + All optional fields will be given default values by the function host when + they are parsed. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of the trigger parameter in the function code. + :param tool_name: The logical tool name exposed to the host. + :param description: Optional human-readable description of the tool. + :param tool_properties: JSON-serialized tool properties/parameters list. + :param data_type: Defines how the Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding JSON. + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_trigger( + trigger=MCPToolTrigger( + name=arg_name, + tool_name=tool_name, + description=description, + tool_properties=tool_properties, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + def dapr_service_invocation_trigger(self, arg_name: str, method_name: str, @@ -3720,6 +3772,57 @@ def decorator(): return wrap + def mcp_tool_input(self, + arg_name: str, + tool_name: str, + description: Optional[str] = None, + tool_properties: Optional[str] = None, + data_type: Optional[Union[DataType, str]] = None, + **kwargs) -> Callable[..., Any]: + """ + The `mcp_tool_input` decorator adds :class:`MCPToolInput` to the + :class:`FunctionBuilder` object for building a :class:`Function` object + used in the worker function indexing model. + + This is equivalent to defining `MCPToolInput` in the `function.json`, + which enables the function to read data from MCP tool sources. + + All optional fields will be assigned default values by the function host + when they are parsed. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of the variable that represents the MCP tool input + object in the function code. + :param tool_name: The logical tool name for the MCP binding. + :param description: Optional human-readable description of the tool. + :param tool_properties: JSON-serialized tool properties/parameters list. + :param data_type: Defines how the Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding JSON. + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=MCPToolInput( + name=arg_name, + tool_name=tool_name, + description=description, + tool_properties=tool_properties, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + def mysql_output(self, arg_name: str, command_text: str, @@ -3771,6 +3874,57 @@ def decorator(): return wrap + def mcp_tool_output(self, + arg_name: str, + tool_name: str, + description: Optional[str] = None, + tool_properties: Optional[str] = None, + data_type: Optional[Union[DataType, str]] = None, + **kwargs) -> Callable[..., Any]: + """ + The `mcp_tool_output` decorator adds :class:`MCPToolOutput` to the + :class:`FunctionBuilder` object for building a :class:`Function` object + used in the worker function indexing model. + + This is equivalent to defining `MCPToolOutput` in the `function.json`, + which enables the function to write data to MCP tool destinations. + + All optional fields will be assigned default values by the function host + when they are parsed. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of the variable that represents the MCP tool output + object in the function code. + :param tool_name: The logical tool name for the MCP binding. + :param description: Optional human-readable description of the tool. + :param tool_properties: JSON-serialized tool properties/parameters list. + :param data_type: Defines how the Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding JSON. + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=MCPToolOutput( + name=arg_name, + tool_name=tool_name, + description=description, + tool_properties=tool_properties, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + class SettingsApi(DecoratorApi, ABC): """Interface to extend for using an existing settings decorator in functions.""" diff --git a/azure/functions/decorators/mcp.py b/azure/functions/decorators/mcp.py new file mode 100644 index 00000000..7a713436 --- /dev/null +++ b/azure/functions/decorators/mcp.py @@ -0,0 +1,64 @@ +from typing import Optional + +from azure.functions.decorators.constants import ( + MCP_TOOL_TRIGGER, MCP_TOOL_INPUT, MCP_TOOL_OUTPUT +) +from azure.functions.decorators.core import Trigger, DataType, InputBinding, \ + OutputBinding + + +class MCPToolTrigger(Trigger): + + @staticmethod + def get_binding_name() -> str: + return MCP_TOOL_TRIGGER + + def __init__(self, + name: str, + tool_name: str, + description: Optional[str] = None, + tool_properties: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.tool_name = tool_name + self.description = description + self.tool_properties = tool_properties + super().__init__(name=name, data_type=data_type) + + +class MCPToolInput(InputBinding): + + @staticmethod + def get_binding_name() -> str: + return MCP_TOOL_INPUT + + def __init__(self, + name: str, + tool_name: str, + description: Optional[str] = None, + tool_properties: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.tool_name = tool_name + self.description = description + self.tool_properties = tool_properties + super().__init__(name=name, data_type=data_type) + + +class MCPToolOutput(OutputBinding): + + @staticmethod + def get_binding_name() -> str: + return MCP_TOOL_OUTPUT + + def __init__(self, + name: str, + tool_name: str, + description: Optional[str] = None, + tool_properties: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.tool_name = tool_name + self.description = description + self.tool_properties = tool_properties + super().__init__(name=name, data_type=data_type) diff --git a/azure/functions/mcp.py b/azure/functions/mcp.py new file mode 100644 index 00000000..da400b0e --- /dev/null +++ b/azure/functions/mcp.py @@ -0,0 +1,67 @@ +import typing + +from . import meta + + +class MCPToolRequest: + """Wrapper for MCP tool trigger payload providing raw & parsed access.""" + + def __init__(self, raw: typing.Any): + self.raw = raw + self.json = None + if isinstance(raw, str): + try: + from ._jsonutils import json as _json + self.json = _json.loads(raw) + except Exception: + self.json = None + elif isinstance(raw, dict): + # If raw is already a dict, use it as the parsed JSON + self.json = raw + + +class MCPToolTriggerConverter(meta.InConverter, binding='mcpToolTrigger', + trigger=True): + + @classmethod + def check_input_type_annotation(cls, pytype: type) -> bool: + return issubclass(pytype, (MCPToolRequest, str)) + + @classmethod + def decode(cls, data: meta.Datum, *, trigger_metadata): + # Handle different data types appropriately + if data.type == 'json': + # If it's already parsed JSON, use the value directly + val = data.value + elif data.type == 'string': + # If it's a string, use it as-is + val = data.value + else: + # Fallback to python_value for other types + val = data.python_value if hasattr(data, 'python_value') else data.value + return MCPToolRequest(val) + + +class MCPToolInputConverter(meta.InConverter, binding='mcpToolInput'): + + @classmethod + def check_input_type_annotation(cls, pytype: type) -> bool: + return issubclass(pytype, (str, MCPToolRequest)) + + @classmethod + def decode(cls, data: meta.Datum, *, trigger_metadata): + val = data.python_value if hasattr(data, 'python_value') else data.value + return val + + +class MCPToolOutputConverter(meta.OutConverter, binding='mcpToolOutput'): + + @classmethod + def check_output_type_annotation(cls, pytype: type) -> bool: + return issubclass(pytype, (str,)) + + @classmethod + def encode(cls, obj: typing.Any, *, expected_type: typing.Optional[type]): + if isinstance(obj, str): + return meta.Datum(type='string', value=obj) + raise NotImplementedError diff --git a/tests/decorators/test_mcp.py b/tests/decorators/test_mcp.py new file mode 100644 index 00000000..4096f16c --- /dev/null +++ b/tests/decorators/test_mcp.py @@ -0,0 +1,76 @@ +import unittest + +from azure.functions import DataType +from azure.functions.decorators.core import BindingDirection +from azure.functions.decorators.mcp import MCPToolTrigger, MCPToolInput, MCPToolOutput +from azure.functions.mcp import MCPToolRequest, MCPToolTriggerConverter, MCPToolOutputConverter +from azure.functions.meta import Datum + + +class TestMCP(unittest.TestCase): + def test_mcp_tool_trigger_valid_creation(self): + trigger = MCPToolTrigger( + name="context", + tool_name="hello", + description="Hello world.", + tool_properties="[]", + data_type=DataType.UNDEFINED, + dummy_field="dummy", + ) + self.assertEqual(trigger.get_binding_name(), "mcpToolTrigger") + self.assertEqual( + trigger.get_dict_repr(), + { + "name": "context", + "toolName": "hello", + "description": "Hello world.", + "toolProperties": "[]", + "type": "mcpToolTrigger", + "dataType": DataType.UNDEFINED, + "dummyField": "dummy", + "direction": BindingDirection.IN, + }, + ) + + def test_mcp_tool_input_valid_creation(self): + ib = MCPToolInput( + name="param", + tool_name="hello", + description="desc", + tool_properties="[]", + data_type=DataType.UNDEFINED, + dummy_field="dummy", + ) + self.assertEqual(ib.get_binding_name(), "mcpToolInput") + d = ib.get_dict_repr() + self.assertEqual(d["toolName"], "hello") + self.assertEqual(d["direction"], BindingDirection.IN) + + def test_mcp_tool_output_valid_creation(self): + ob = MCPToolOutput( + name="out", + tool_name="hello", + description="desc", + tool_properties="[]", + data_type=DataType.UNDEFINED, + ) + self.assertEqual(ob.get_binding_name(), "mcpToolOutput") + d = ob.get_dict_repr() + self.assertEqual(d["direction"], BindingDirection.OUT) + + def test_trigger_converter(self): + # Test with string data + datum = Datum(value='{"arguments":{}}', type='string') + req = MCPToolTriggerConverter.decode(datum, trigger_metadata={}) + self.assertTrue(isinstance(req, MCPToolRequest)) + self.assertIsNotNone(req.json) + + # Test with json data + datum_json = Datum(value={"arguments": {}}, type='json') + req_json = MCPToolTriggerConverter.decode(datum_json, trigger_metadata={}) + self.assertTrue(isinstance(req_json, MCPToolRequest)) + + def test_output_converter(self): + datum = MCPToolOutputConverter.encode("result", expected_type=str) + self.assertEqual(datum.type, 'string') + self.assertEqual(datum.value, 'result') From 51503cdc4c789714a5535e8694f2470ce758be6f Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Tue, 19 Aug 2025 14:19:20 -0500 Subject: [PATCH 2/4] Flake8 fixes --- azure/functions/decorators/mcp.py | 4 ++-- azure/functions/mcp.py | 2 +- tests/decorators/test_mcp.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/azure/functions/decorators/mcp.py b/azure/functions/decorators/mcp.py index 7a713436..9d46c525 100644 --- a/azure/functions/decorators/mcp.py +++ b/azure/functions/decorators/mcp.py @@ -27,7 +27,7 @@ def __init__(self, class MCPToolInput(InputBinding): - + @staticmethod def get_binding_name() -> str: return MCP_TOOL_INPUT @@ -46,7 +46,7 @@ def __init__(self, class MCPToolOutput(OutputBinding): - + @staticmethod def get_binding_name() -> str: return MCP_TOOL_OUTPUT diff --git a/azure/functions/mcp.py b/azure/functions/mcp.py index da400b0e..d40fa8ee 100644 --- a/azure/functions/mcp.py +++ b/azure/functions/mcp.py @@ -21,7 +21,7 @@ def __init__(self, raw: typing.Any): class MCPToolTriggerConverter(meta.InConverter, binding='mcpToolTrigger', - trigger=True): + trigger=True): @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: diff --git a/tests/decorators/test_mcp.py b/tests/decorators/test_mcp.py index 4096f16c..deacca30 100644 --- a/tests/decorators/test_mcp.py +++ b/tests/decorators/test_mcp.py @@ -64,8 +64,8 @@ def test_trigger_converter(self): req = MCPToolTriggerConverter.decode(datum, trigger_metadata={}) self.assertTrue(isinstance(req, MCPToolRequest)) self.assertIsNotNone(req.json) - - # Test with json data + + # Test with json data datum_json = Datum(value={"arguments": {}}, type='json') req_json = MCPToolTriggerConverter.decode(datum_json, trigger_metadata={}) self.assertTrue(isinstance(req_json, MCPToolRequest)) From 3c3bea9f9748e48867aaf338fbb4b916e0ec0841 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 28 Aug 2025 12:17:27 -0500 Subject: [PATCH 3/4] Final changes --- azure/functions/__init__.py | 6 -- azure/functions/decorators/constants.py | 2 - azure/functions/decorators/function_app.py | 106 +-------------------- azure/functions/decorators/mcp.py | 43 +-------- azure/functions/mcp.py | 71 ++++++-------- tests/decorators/test_mcp.py | 46 ++------- 6 files changed, 39 insertions(+), 235 deletions(-) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index ccf99408..cbec6f86 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -39,11 +39,6 @@ from . import sql # NoQA from . import warmup # NoQA from . import mysql # NoQA -from . import mcp # NoQA -from .decorators.mcp import MCPToolTrigger, MCPToolInput, MCPToolOutput # NoQA -from .mcp import MCPToolRequest, MCPToolTriggerConverter # NoQA -from .mcp import MCPToolRequest, MCPToolTriggerConverter, MCPToolInputConverter, MCPToolOutputConverter # NoQA - __all__ = ( # Functions @@ -75,7 +70,6 @@ 'WarmUpContext', 'MySqlRow', 'MySqlRowList', - 'MCPToolRequest', # Middlewares 'WsgiMiddleware', diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index b48fe2aa..c9091128 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -46,5 +46,3 @@ MYSQL = "mysql" MYSQL_TRIGGER = "mysqlTrigger" MCP_TOOL_TRIGGER = "mcpToolTrigger" -MCP_TOOL_INPUT = "mcpToolInput" -MCP_TOOL_OUTPUT = "mcpToolOutput" diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 034f7084..20961621 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -42,7 +42,7 @@ AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, \ semantic_search_system_prompt, \ SemanticSearchInput, EmbeddingsStoreOutput -from .mcp import MCPToolTrigger, MCPToolInput, MCPToolOutput +from .mcp import MCPToolTrigger from .retry_policy import RetryPolicy from .function_name import FunctionName from .warmup import WarmUpTrigger @@ -1531,7 +1531,7 @@ def mcp_tool_trigger(self, All optional fields will be given default values by the function host when they are parsed. - Ref: https://aka.ms/azure-function-binding-custom + Ref: https://aka.ms/remote-mcp-functions-python :param arg_name: The name of the trigger parameter in the function code. :param tool_name: The logical tool name exposed to the host. @@ -3772,57 +3772,6 @@ def decorator(): return wrap - def mcp_tool_input(self, - arg_name: str, - tool_name: str, - description: Optional[str] = None, - tool_properties: Optional[str] = None, - data_type: Optional[Union[DataType, str]] = None, - **kwargs) -> Callable[..., Any]: - """ - The `mcp_tool_input` decorator adds :class:`MCPToolInput` to the - :class:`FunctionBuilder` object for building a :class:`Function` object - used in the worker function indexing model. - - This is equivalent to defining `MCPToolInput` in the `function.json`, - which enables the function to read data from MCP tool sources. - - All optional fields will be assigned default values by the function host - when they are parsed. - - Ref: https://aka.ms/azure-function-binding-custom - - :param arg_name: The name of the variable that represents the MCP tool input - object in the function code. - :param tool_name: The logical tool name for the MCP binding. - :param description: Optional human-readable description of the tool. - :param tool_properties: JSON-serialized tool properties/parameters list. - :param data_type: Defines how the Functions runtime should treat the - parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding JSON. - - :return: Decorator function. - """ - - @self._configure_function_builder - def wrap(fb): - def decorator(): - fb.add_binding( - binding=MCPToolInput( - name=arg_name, - tool_name=tool_name, - description=description, - tool_properties=tool_properties, - data_type=parse_singular_param_to_enum(data_type, - DataType), - **kwargs)) - return fb - - return decorator() - - return wrap - def mysql_output(self, arg_name: str, command_text: str, @@ -3874,57 +3823,6 @@ def decorator(): return wrap - def mcp_tool_output(self, - arg_name: str, - tool_name: str, - description: Optional[str] = None, - tool_properties: Optional[str] = None, - data_type: Optional[Union[DataType, str]] = None, - **kwargs) -> Callable[..., Any]: - """ - The `mcp_tool_output` decorator adds :class:`MCPToolOutput` to the - :class:`FunctionBuilder` object for building a :class:`Function` object - used in the worker function indexing model. - - This is equivalent to defining `MCPToolOutput` in the `function.json`, - which enables the function to write data to MCP tool destinations. - - All optional fields will be assigned default values by the function host - when they are parsed. - - Ref: https://aka.ms/azure-function-binding-custom - - :param arg_name: The name of the variable that represents the MCP tool output - object in the function code. - :param tool_name: The logical tool name for the MCP binding. - :param description: Optional human-readable description of the tool. - :param tool_properties: JSON-serialized tool properties/parameters list. - :param data_type: Defines how the Functions runtime should treat the - parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding JSON. - - :return: Decorator function. - """ - - @self._configure_function_builder - def wrap(fb): - def decorator(): - fb.add_binding( - binding=MCPToolOutput( - name=arg_name, - tool_name=tool_name, - description=description, - tool_properties=tool_properties, - data_type=parse_singular_param_to_enum(data_type, - DataType), - **kwargs)) - return fb - - return decorator() - - return wrap - class SettingsApi(DecoratorApi, ABC): """Interface to extend for using an existing settings decorator in functions.""" diff --git a/azure/functions/decorators/mcp.py b/azure/functions/decorators/mcp.py index 9d46c525..7657975d 100644 --- a/azure/functions/decorators/mcp.py +++ b/azure/functions/decorators/mcp.py @@ -1,10 +1,9 @@ from typing import Optional from azure.functions.decorators.constants import ( - MCP_TOOL_TRIGGER, MCP_TOOL_INPUT, MCP_TOOL_OUTPUT + MCP_TOOL_TRIGGER ) -from azure.functions.decorators.core import Trigger, DataType, InputBinding, \ - OutputBinding +from azure.functions.decorators.core import Trigger, DataType class MCPToolTrigger(Trigger): @@ -24,41 +23,3 @@ def __init__(self, self.description = description self.tool_properties = tool_properties super().__init__(name=name, data_type=data_type) - - -class MCPToolInput(InputBinding): - - @staticmethod - def get_binding_name() -> str: - return MCP_TOOL_INPUT - - def __init__(self, - name: str, - tool_name: str, - description: Optional[str] = None, - tool_properties: Optional[str] = None, - data_type: Optional[DataType] = None, - **kwargs): - self.tool_name = tool_name - self.description = description - self.tool_properties = tool_properties - super().__init__(name=name, data_type=data_type) - - -class MCPToolOutput(OutputBinding): - - @staticmethod - def get_binding_name() -> str: - return MCP_TOOL_OUTPUT - - def __init__(self, - name: str, - tool_name: str, - description: Optional[str] = None, - tool_properties: Optional[str] = None, - data_type: Optional[DataType] = None, - **kwargs): - self.tool_name = tool_name - self.description = description - self.tool_properties = tool_properties - super().__init__(name=name, data_type=data_type) diff --git a/azure/functions/mcp.py b/azure/functions/mcp.py index d40fa8ee..839d94ef 100644 --- a/azure/functions/mcp.py +++ b/azure/functions/mcp.py @@ -3,65 +3,48 @@ from . import meta -class MCPToolRequest: - """Wrapper for MCP tool trigger payload providing raw & parsed access.""" - - def __init__(self, raw: typing.Any): - self.raw = raw - self.json = None - if isinstance(raw, str): - try: - from ._jsonutils import json as _json - self.json = _json.loads(raw) - except Exception: - self.json = None - elif isinstance(raw, dict): - # If raw is already a dict, use it as the parsed JSON - self.json = raw - - class MCPToolTriggerConverter(meta.InConverter, binding='mcpToolTrigger', trigger=True): @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: - return issubclass(pytype, (MCPToolRequest, str)) + return issubclass(pytype, (str, dict, bytes)) + + @classmethod + def has_implicit_output(cls) -> bool: + return True @classmethod def decode(cls, data: meta.Datum, *, trigger_metadata): + """ + Decode incoming MCP tool request data. + Returns the raw data in its native format (string, dict, bytes). + """ # Handle different data types appropriately if data.type == 'json': # If it's already parsed JSON, use the value directly - val = data.value + return data.value elif data.type == 'string': # If it's a string, use it as-is - val = data.value + return data.value + elif data.type == 'bytes': + return data.value else: # Fallback to python_value for other types - val = data.python_value if hasattr(data, 'python_value') else data.value - return MCPToolRequest(val) - - -class MCPToolInputConverter(meta.InConverter, binding='mcpToolInput'): - - @classmethod - def check_input_type_annotation(cls, pytype: type) -> bool: - return issubclass(pytype, (str, MCPToolRequest)) + return data.python_value if hasattr(data, 'python_value') else data.value @classmethod - def decode(cls, data: meta.Datum, *, trigger_metadata): - val = data.python_value if hasattr(data, 'python_value') else data.value - return val - - -class MCPToolOutputConverter(meta.OutConverter, binding='mcpToolOutput'): - - @classmethod - def check_output_type_annotation(cls, pytype: type) -> bool: - return issubclass(pytype, (str,)) - - @classmethod - def encode(cls, obj: typing.Any, *, expected_type: typing.Optional[type]): - if isinstance(obj, str): + def encode(cls, obj: typing.Any, *, expected_type: typing.Optional[type] = None): + """ + Encode the return value from MCP tool functions. + MCP tools typically return string responses. + """ + if obj is None: + return meta.Datum(type='string', value='') + elif isinstance(obj, str): return meta.Datum(type='string', value=obj) - raise NotImplementedError + elif isinstance(obj, (bytes, bytearray)): + return meta.Datum(type='bytes', value=bytes(obj)) + else: + # Convert other types to string + return meta.Datum(type='string', value=str(obj)) diff --git a/tests/decorators/test_mcp.py b/tests/decorators/test_mcp.py index deacca30..044be213 100644 --- a/tests/decorators/test_mcp.py +++ b/tests/decorators/test_mcp.py @@ -2,8 +2,8 @@ from azure.functions import DataType from azure.functions.decorators.core import BindingDirection -from azure.functions.decorators.mcp import MCPToolTrigger, MCPToolInput, MCPToolOutput -from azure.functions.mcp import MCPToolRequest, MCPToolTriggerConverter, MCPToolOutputConverter +from azure.functions.decorators.mcp import MCPToolTrigger +from azure.functions.mcp import MCPToolTriggerConverter from azure.functions.meta import Datum @@ -32,45 +32,15 @@ def test_mcp_tool_trigger_valid_creation(self): }, ) - def test_mcp_tool_input_valid_creation(self): - ib = MCPToolInput( - name="param", - tool_name="hello", - description="desc", - tool_properties="[]", - data_type=DataType.UNDEFINED, - dummy_field="dummy", - ) - self.assertEqual(ib.get_binding_name(), "mcpToolInput") - d = ib.get_dict_repr() - self.assertEqual(d["toolName"], "hello") - self.assertEqual(d["direction"], BindingDirection.IN) - - def test_mcp_tool_output_valid_creation(self): - ob = MCPToolOutput( - name="out", - tool_name="hello", - description="desc", - tool_properties="[]", - data_type=DataType.UNDEFINED, - ) - self.assertEqual(ob.get_binding_name(), "mcpToolOutput") - d = ob.get_dict_repr() - self.assertEqual(d["direction"], BindingDirection.OUT) - def test_trigger_converter(self): # Test with string data datum = Datum(value='{"arguments":{}}', type='string') - req = MCPToolTriggerConverter.decode(datum, trigger_metadata={}) - self.assertTrue(isinstance(req, MCPToolRequest)) - self.assertIsNotNone(req.json) + result = MCPToolTriggerConverter.decode(datum, trigger_metadata={}) + self.assertEqual(result, '{"arguments":{}}') + self.assertIsInstance(result, str) # Test with json data datum_json = Datum(value={"arguments": {}}, type='json') - req_json = MCPToolTriggerConverter.decode(datum_json, trigger_metadata={}) - self.assertTrue(isinstance(req_json, MCPToolRequest)) - - def test_output_converter(self): - datum = MCPToolOutputConverter.encode("result", expected_type=str) - self.assertEqual(datum.type, 'string') - self.assertEqual(datum.value, 'result') + result_json = MCPToolTriggerConverter.decode(datum_json, trigger_metadata={}) + self.assertEqual(result_json, {"arguments": {}}) + self.assertIsInstance(result_json, dict) From 2e32d5cd8e8f5e3b92811719570d5ce80895cf09 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 28 Aug 2025 12:18:50 -0500 Subject: [PATCH 4/4] Final Final changes --- azure/functions/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index cbec6f86..ee2a01f1 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -40,6 +40,7 @@ from . import warmup # NoQA from . import mysql # NoQA + __all__ = ( # Functions 'get_binding_registry',