4343 AssistantQueryInput , AssistantPostInput , InputType , EmbeddingsInput , \
4444 semantic_search_system_prompt , \
4545 SemanticSearchInput , EmbeddingsStoreOutput
46- from .mcp import MCPToolTrigger , MCPToolContext , _TYPE_MAPPING , _extract_type_and_description , _get_user_function
46+ from .mcp import MCPToolTrigger , MCPToolContext , _TYPE_MAPPING , _extract_type_and_description
4747from .retry_policy import RetryPolicy
4848from .function_name import FunctionName
4949from .warmup import WarmUpTrigger
5252from azure .functions .decorators .mysql import MySqlInput , MySqlOutput , \
5353 MySqlTrigger
5454
55+ _logger = logging .getLogger ('azure.functions.AsgiMiddleware' )
5556
5657class Function (object ):
5758 """
@@ -273,6 +274,7 @@ def _validate_function(self,
273274 trigger = self ._function .get_trigger ()
274275 if trigger is None :
275276 raise ValueError (
277+ f"This is the function: { self ._function } "
276278 f"Function { function_name } does not have a trigger. A valid "
277279 f"function must have one and only one trigger registered." )
278280
@@ -463,8 +465,7 @@ def auth_level(self) -> AuthLevel:
463465
464466class TriggerApi (DecoratorApi , ABC ):
465467 """Interface to extend for using existing trigger decorator functions."""
466- def mcp_tool (self ) -> Callable [[Callable ], Callable ]:
467- """
468+ """
468469 Decorator to register an MCP tool function.
469470
470471 Automatically:
@@ -473,70 +474,86 @@ def mcp_tool(self) -> Callable[[Callable], Callable]:
473474 - Extracts parameters and types for tool properties
474475 - Handles MCPToolContext injection
475476 """
476- def decorator (user_func : Callable ) -> Callable :
477- target_func = _get_user_function (user_func )
477+ def mcp_tool (self ):
478+ @self ._configure_function_builder
479+ def decorator (fb : FunctionBuilder ) -> FunctionBuilder :
480+ target_func = fb ._function .get_user_function ()
478481 sig = inspect .signature (target_func )
479482 tool_name = target_func .__name__
480483 description = (target_func .__doc__ or "" ).strip ().split ("\n " )[0 ]
481484
482- # Build tool properties metadata
485+ bound_param_names = {b .name for b in getattr (fb ._function , "_bindings" , [])}
486+ skip_param_names = bound_param_names
487+ _logger .info ("Bound param names for %s: %s" , tool_name , skip_param_names )
488+
489+ # Build tool properties
483490 tool_properties = []
484491 for param_name , param in sig .parameters .items ():
492+ if param_name in skip_param_names :
493+ continue
485494 param_type_hint = param .annotation if param .annotation != inspect .Parameter .empty else str
486- actual_type , param_description = _extract_type_and_description (param_name , param_type_hint )
495+ actual_type , param_desc = _extract_type_and_description (param_name , param_type_hint )
487496 if actual_type is MCPToolContext :
488497 continue
489498 property_type = _TYPE_MAPPING .get (actual_type , "string" )
490499 tool_properties .append ({
491500 "propertyName" : param_name ,
492501 "propertyType" : property_type ,
493- "description" : param_description ,
502+ "description" : param_desc ,
494503 })
495504
496- tool_properties_json = json .dumps (tool_properties )
497-
498- # Wrapper function for MCP trigger
499- def wrapper (context : str ) -> str :
500- try :
501- content = json .loads (context )
502- arguments = content .get ("arguments" , {})
503- kwargs = {}
504-
505- for param_name , param in sig .parameters .items ():
506- param_type_hint = param .annotation if param .annotation != inspect .Parameter .empty else str
507- actual_type , _ = _extract_type_and_description (param_name , param_type_hint )
508-
509- if actual_type is MCPToolContext :
510- kwargs [param_name ] = content
511- elif param_name in arguments :
512- kwargs [param_name ] = arguments [param_name ]
513- else :
514- return f"Error: Missing required parameter '{ param_name } ' for '{ tool_name } '"
515-
516- result = target_func (** kwargs )
517- return str (result )
518-
519- except Exception as e :
520- return f"Error executing function '{ tool_name } ': { str (e )} "
521-
522- wrapper .__name__ = target_func .__name__
523- wrapper .__doc__ = target_func .__doc__
524-
525- # Use the existing FunctionRegister mechanism to add the trigger
526- fb = self ._configure_function_builder (lambda fb : fb )(wrapper )
505+ tool_properties_json = json .dumps (tool_properties )\
506+
507+ bound_params = [
508+ inspect .Parameter (name , inspect .Parameter .POSITIONAL_OR_KEYWORD )
509+ for name in bound_param_names
510+ ]
511+ wrapper_sig = inspect .Signature ([
512+ * bound_params ,
513+ inspect .Parameter ("context" , inspect .Parameter .POSITIONAL_OR_KEYWORD )
514+ ])
515+
516+ # Wrap the original function
517+ import functools
518+ @functools .wraps (target_func )
519+ async def wrapper (context : str , * args , ** kwargs ):
520+ _logger .info (f"Invoking MCP tool function '{ tool_name } ' with context: { context } " )
521+ content = json .loads (context )
522+ arguments = content .get ("arguments" , {})
523+ call_kwargs = {}
524+ for param_name , param in sig .parameters .items ():
525+ param_type_hint = param .annotation if param .annotation != inspect .Parameter .empty else str
526+ actual_type , _ = _extract_type_and_description (param_name , param_type_hint )
527+ if actual_type is MCPToolContext :
528+ call_kwargs [param_name ] = content
529+ elif param_name in arguments :
530+ call_kwargs [param_name ] = arguments [param_name ]
531+ call_kwargs .update (kwargs )
532+ result = target_func (** call_kwargs )
533+ if asyncio .iscoroutine (result ):
534+ result = await result
535+ return str (result )
536+
537+ wrapper .__signature__ = wrapper_sig
538+ fb ._function ._func = wrapper
539+ _logger .info (f"Registered MCP tool function '{ tool_name } ' with description: { description } and properties: { tool_properties_json } " )
540+
541+ # Add the MCP trigger
527542 fb .add_trigger (
528543 trigger = MCPToolTrigger (
529544 name = "context" ,
530545 tool_name = tool_name ,
531546 description = description ,
532- tool_properties = tool_properties_json
547+ tool_properties = tool_properties_json ,
533548 )
534549 )
535-
536550 return fb
537551
538552 return decorator
539553
554+
555+
556+
540557 def route (self ,
541558 route : Optional [str ] = None ,
542559 trigger_arg_name : str = 'req' ,
@@ -3971,6 +3988,9 @@ def get_functions(self) -> List[Function]:
39713988
39723989 :return: A list of :class:`Function` objects defined in the app.
39733990 """
3991+ for function_builder in self ._function_builders :
3992+ _logger .info ("Function builder functions: %s" ,
3993+ function_builder ._function )
39743994 functions = [function_builder .build (self .auth_level )
39753995 for function_builder in self ._function_builders ]
39763996
@@ -4193,3 +4213,27 @@ def _add_http_app(self,
41934213 route = "/{*route}" )
41944214 def http_app_func (req : HttpRequest , context : Context ):
41954215 return wsgi_middleware .handle (req , context )
4216+
4217+ def _get_user_function (target_func ):
4218+ """
4219+ Unwraps decorated or builder-wrapped functions to find the original
4220+ user-defined function (the one starting with 'def' or 'async def').
4221+ """
4222+ # Case 1: It's a FunctionBuilder object
4223+ if isinstance (target_func , FunctionBuilder ):
4224+ # Access the internal user function
4225+ try :
4226+ return target_func ._function .get_user_function ()
4227+ except AttributeError :
4228+ pass
4229+
4230+ # Case 2: It's already the user-defined function
4231+ if callable (target_func ) and hasattr (target_func , "__name__" ):
4232+ return target_func
4233+
4234+ # Case 3: It might be a partially wrapped callable
4235+ if hasattr (target_func , "__wrapped__" ):
4236+ return _get_user_function (target_func .__wrapped__ )
4237+
4238+ # Default fallback
4239+ return target_func
0 commit comments