Skip to content

Commit 3ba01e8

Browse files
committed
works with bindings - no plain context yet
1 parent 82212b6 commit 3ba01e8

File tree

1 file changed

+85
-41
lines changed

1 file changed

+85
-41
lines changed

azure/functions/decorators/function_app.py

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
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
4747
from .retry_policy import RetryPolicy
4848
from .function_name import FunctionName
4949
from .warmup import WarmUpTrigger
@@ -52,6 +52,7 @@
5252
from azure.functions.decorators.mysql import MySqlInput, MySqlOutput, \
5353
MySqlTrigger
5454

55+
_logger = logging.getLogger('azure.functions.AsgiMiddleware')
5556

5657
class 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

464466
class 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

Comments
 (0)