22# Licensed under the MIT License.
33import abc
44import asyncio
5+ import functools
56import inspect
67import json
78import logging
5354from azure .functions .decorators .mysql import MySqlInput , MySqlOutput , \
5455 MySqlTrigger
5556
56- _logger = logging .getLogger ('azure.functions.AsgiMiddleware' )
5757
5858class Function (object ):
5959 """
@@ -466,94 +466,6 @@ def auth_level(self) -> AuthLevel:
466466
467467class TriggerApi (DecoratorApi , ABC ):
468468 """Interface to extend for using existing trigger decorator functions."""
469- """
470- Decorator to register an MCP tool function.
471-
472- Automatically:
473- - Infers tool name from function name
474- - Extracts first line of docstring as description
475- - Extracts parameters and types for tool properties
476- - Handles MCPToolContext injection
477- """
478- def mcp_tool (self ):
479- @self ._configure_function_builder
480- def decorator (fb : FunctionBuilder ) -> FunctionBuilder :
481- target_func = fb ._function .get_user_function ()
482- sig = inspect .signature (target_func )
483- tool_name = target_func .__name__
484- description = (target_func .__doc__ or "" ).strip ().split ("\n " )[0 ]
485-
486- bound_param_names = {b .name for b in getattr (fb ._function , "_bindings" , [])}
487- skip_param_names = bound_param_names
488- _logger .info ("Bound param names for %s: %s" , tool_name , skip_param_names )
489-
490- # Build tool properties
491- tool_properties = []
492- for param_name , param in sig .parameters .items ():
493- if param_name in skip_param_names :
494- continue
495- param_type_hint = param .annotation if param .annotation != inspect .Parameter .empty else str
496- actual_type , param_desc = _extract_type_and_description (param_name , param_type_hint )
497- if actual_type is MCPToolContext :
498- continue
499- property_type = _TYPE_MAPPING .get (actual_type , "string" )
500- tool_properties .append ({
501- "propertyName" : param_name ,
502- "propertyType" : property_type ,
503- "description" : param_desc ,
504- })
505-
506- tool_properties_json = json .dumps (tool_properties )
507-
508- bound_params = [
509- inspect .Parameter (name , inspect .Parameter .POSITIONAL_OR_KEYWORD )
510- for name in bound_param_names
511- ]
512- wrapper_sig = inspect .Signature ([
513- * bound_params ,
514- inspect .Parameter ("context" , inspect .Parameter .POSITIONAL_OR_KEYWORD )
515- ])
516-
517- # Wrap the original function
518- import functools
519- @functools .wraps (target_func )
520- async def wrapper (context : str , * args , ** kwargs ):
521- _logger .info (f"Invoking MCP tool function '{ tool_name } ' with context: { context } " )
522- content = json .loads (context )
523- arguments = content .get ("arguments" , {})
524- call_kwargs = {}
525- for param_name , param in sig .parameters .items ():
526- param_type_hint = param .annotation if param .annotation != inspect .Parameter .empty else str
527- actual_type , _ = _extract_type_and_description (param_name , param_type_hint )
528- if actual_type is MCPToolContext :
529- call_kwargs [param_name ] = content
530- elif param_name in arguments :
531- call_kwargs [param_name ] = arguments [param_name ]
532- call_kwargs .update (kwargs )
533- result = target_func (** call_kwargs )
534- if asyncio .iscoroutine (result ):
535- result = await result
536- return str (result )
537-
538- wrapper .__signature__ = wrapper_sig
539- fb ._function ._func = wrapper
540- _logger .info (f"Registered MCP tool function '{ tool_name } ' with description: { description } and properties: { tool_properties_json } " )
541-
542- # Add the MCP trigger
543- fb .add_trigger (
544- trigger = MCPToolTrigger (
545- name = "context" ,
546- tool_name = tool_name ,
547- description = description ,
548- tool_properties = tool_properties_json ,
549- )
550- )
551- return fb
552-
553- return decorator
554-
555-
556-
557469
558470 def route (self ,
559471 route : Optional [str ] = None ,
@@ -1655,6 +1567,93 @@ def decorator():
16551567
16561568 return wrap
16571569
1570+ def mcp_tool (self ):
1571+ """
1572+ Decorator to register an MCP tool function.
1573+
1574+ Automatically:
1575+ - Infers tool name from function name
1576+ - Extracts first line of docstring as description
1577+ - Extracts parameters and types for tool properties
1578+ - Handles MCPToolContext injection
1579+ """
1580+ @self ._configure_function_builder
1581+ def decorator (fb : FunctionBuilder ) -> FunctionBuilder :
1582+ target_func = fb ._function .get_user_function ()
1583+ sig = inspect .signature (target_func )
1584+ # Parse tool name and description from function signature
1585+ tool_name = target_func .__name__
1586+ description = (target_func .__doc__ or "" ).strip ().split ("\n " )[0 ]
1587+
1588+ # Identify arguments that are already bound (bindings)
1589+ bound_param_names = {b .name for b in getattr (fb ._function , "_bindings" , [])}
1590+ skip_param_names = bound_param_names
1591+
1592+ # Build tool properties
1593+ tool_properties = []
1594+ for param_name , param in sig .parameters .items ():
1595+ if param_name in skip_param_names :
1596+ continue
1597+ param_type_hint = param .annotation if param .annotation != inspect .Parameter .empty else str # noqa
1598+ # Parse type and description from type hint
1599+ actual_type , param_desc = _extract_type_and_description (
1600+ param_name , param_type_hint )
1601+ if actual_type is MCPToolContext :
1602+ continue
1603+ property_type = _TYPE_MAPPING .get (actual_type , "string" )
1604+ tool_properties .append ({
1605+ "propertyName" : param_name ,
1606+ "propertyType" : property_type ,
1607+ "description" : param_desc ,
1608+ })
1609+
1610+ tool_properties_json = json .dumps (tool_properties )
1611+
1612+ bound_params = [
1613+ inspect .Parameter (name , inspect .Parameter .POSITIONAL_OR_KEYWORD )
1614+ for name in bound_param_names
1615+ ]
1616+ # Build new signature for the wrapper function to pass worker indexing
1617+ wrapper_sig = inspect .Signature ([
1618+ * bound_params ,
1619+ inspect .Parameter ("context" , inspect .Parameter .POSITIONAL_OR_KEYWORD )
1620+ ])
1621+
1622+ # Wrap the original function
1623+ @functools .wraps (target_func )
1624+ async def wrapper (context : str , * args , ** kwargs ):
1625+ content = json .loads (context )
1626+ arguments = content .get ("arguments" , {})
1627+ call_kwargs = {}
1628+ for param_name , param in sig .parameters .items ():
1629+ param_type_hint = param .annotation if param .annotation != inspect .Parameter .empty else str # noqa
1630+ actual_type , _ = _extract_type_and_description (param_name , param_type_hint )
1631+ if actual_type is MCPToolContext :
1632+ call_kwargs [param_name ] = content
1633+ elif param_name in arguments :
1634+ call_kwargs [param_name ] = arguments [param_name ]
1635+ call_kwargs .update (kwargs )
1636+ result = target_func (** call_kwargs )
1637+ if asyncio .iscoroutine (result ):
1638+ result = await result
1639+ return str (result )
1640+
1641+ wrapper .__signature__ = wrapper_sig
1642+ fb ._function ._func = wrapper
1643+
1644+ # Add the MCP trigger
1645+ fb .add_trigger (
1646+ trigger = MCPToolTrigger (
1647+ name = "context" ,
1648+ tool_name = tool_name ,
1649+ description = description ,
1650+ tool_properties = tool_properties_json ,
1651+ )
1652+ )
1653+ return fb
1654+
1655+ return decorator
1656+
16581657 def dapr_service_invocation_trigger (self ,
16591658 arg_name : str ,
16601659 method_name : str ,
@@ -3989,9 +3988,6 @@ def get_functions(self) -> List[Function]:
39893988
39903989 :return: A list of :class:`Function` objects defined in the app.
39913990 """
3992- for function_builder in self ._function_builders :
3993- _logger .info ("Function builder functions: %s" ,
3994- function_builder ._function )
39953991 functions = [function_builder .build (self .auth_level )
39963992 for function_builder in self ._function_builders ]
39973993
@@ -4215,6 +4211,7 @@ def _add_http_app(self,
42154211 def http_app_func (req : HttpRequest , context : Context ):
42164212 return wsgi_middleware .handle (req , context )
42174213
4214+
42184215def _get_user_function (target_func ):
42194216 """
42204217 Unwraps decorated or builder-wrapped functions to find the original
@@ -4237,4 +4234,4 @@ def _get_user_function(target_func):
42374234 return _get_user_function (target_func .__wrapped__ )
42384235
42394236 # Default fallback
4240- return target_func
4237+ return target_func
0 commit comments