Skip to content

Commit 8476f18

Browse files
committed
lint
1 parent 4b95af5 commit 8476f18

File tree

3 files changed

+94
-96
lines changed

3 files changed

+94
-96
lines changed

azure/functions/decorators/function_app.py

Lines changed: 90 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Licensed under the MIT License.
33
import abc
44
import asyncio
5+
import functools
56
import inspect
67
import json
78
import logging
@@ -53,7 +54,6 @@
5354
from azure.functions.decorators.mysql import MySqlInput, MySqlOutput, \
5455
MySqlTrigger
5556

56-
_logger = logging.getLogger('azure.functions.AsgiMiddleware')
5757

5858
class Function(object):
5959
"""
@@ -466,94 +466,6 @@ def auth_level(self) -> AuthLevel:
466466

467467
class 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+
42184215
def _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

azure/functions/decorators/mcp.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Optional
2-
from typing import Any, Dict, Tuple, get_args, get_origin, Annotated
3-
import logging
2+
from typing import Any, Tuple, get_args, get_origin, Annotated
43

54
from azure.functions.decorators.constants import (
65
MCP_TOOL_TRIGGER
@@ -15,6 +14,7 @@
1514
bool: "boolean",
1615
}
1716

17+
1818
class MCPToolTrigger(Trigger):
1919

2020
@staticmethod
@@ -40,6 +40,6 @@ def _extract_type_and_description(param_name: str, type_hint: Any) -> Tuple[Any,
4040
args = get_args(type_hint)
4141
actual_type = args[0]
4242
# Use first string annotation as description if present
43-
param_description = next((a for a in args[1:] if isinstance(a, str)), f"The {param_name} parameter.")
43+
param_description = next((a for a in args[1:] if isinstance(a, str)), f"The {param_name} parameter.") # noqa
4444
return actual_type, param_description
4545
return type_hint, f"The {param_name} parameter."

azure/functions/mcp.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# MCP-specific context object
77
class MCPToolContext(typing.Dict[str, typing.Any]):
88
"""Injected context object for MCP tool triggers."""
9+
910
pass
1011

1112

0 commit comments

Comments
 (0)