Skip to content

Commit fd4e269

Browse files
committed
Add support for isRequired, isArray, McpToolProperty input
1 parent 7683197 commit fd4e269

File tree

2 files changed

+111
-6
lines changed

2 files changed

+111
-6
lines changed

azure/functions/decorators/function_app.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, \
4545
semantic_search_system_prompt, \
4646
SemanticSearchInput, EmbeddingsStoreOutput
47-
from .mcp import MCPToolTrigger, _TYPE_MAPPING
47+
from .mcp import MCPToolTrigger, _TYPE_MAPPING, check_property_type, check_is_array, check_is_required
4848
from .retry_policy import RetryPolicy
4949
from .function_name import FunctionName
5050
from .warmup import WarmUpTrigger
@@ -55,6 +55,8 @@
5555
MySqlTrigger
5656

5757

58+
logger = logging.getLogger('azure.functions.WsgiMiddleware')
59+
5860
class Function(object):
5961
"""
6062
The function object represents a function in Function App. It
@@ -1588,6 +1590,11 @@ def mcp_tool(self):
15881590
def decorator(fb: FunctionBuilder) -> FunctionBuilder:
15891591
target_func = fb._function.get_user_function()
15901592
sig = inspect.signature(target_func)
1593+
1594+
# Pull any explicitly declared MCP tool properties
1595+
explicit_properties = getattr(target_func, "__mcp_tool_properties__", {})
1596+
logger.info(f"Explicit MCP tool properties: {explicit_properties}")
1597+
15911598
# Parse tool name and description from function signature
15921599
tool_name = target_func.__name__
15931600
description = (target_func.__doc__ or "").strip().split("\n")[0]
@@ -1602,15 +1609,29 @@ def decorator(fb: FunctionBuilder) -> FunctionBuilder:
16021609
if param_name in skip_param_names:
16031610
continue
16041611
param_type_hint = param.annotation if param.annotation != inspect.Parameter.empty else str # noqa
1605-
# Parse type and description from type hint
1606-
actual_type = param_type_hint
1607-
if actual_type is MCPToolContext:
1612+
1613+
if param_type_hint is MCPToolContext:
16081614
continue
1609-
property_type = _TYPE_MAPPING.get(actual_type, "string")
1615+
1616+
# Check if explicit metadata exists for this param
1617+
if param_name in explicit_properties:
1618+
logger.info(f"Using explicit MCP tool property for param: {param_name}") # noqa
1619+
prop = explicit_properties[param_name].copy()
1620+
prop["propertyName"] = param_name
1621+
tool_properties.append(prop)
1622+
continue
1623+
1624+
# Otherwise infer it
1625+
is_required = check_is_required(param, param_type_hint)
1626+
is_array = check_is_array(param_type_hint)
1627+
property_type = check_property_type(param_type_hint, is_array)
1628+
16101629
tool_properties.append({
16111630
"propertyName": param_name,
16121631
"propertyType": property_type,
16131632
"description": "",
1633+
"isArray": is_array,
1634+
"isRequired": is_required
16141635
})
16151636

16161637
tool_properties_json = json.dumps(tool_properties)
@@ -1660,6 +1681,40 @@ async def wrapper(context: str, *args, **kwargs):
16601681

16611682
return decorator
16621683

1684+
def mcp_tool_property(self, arg_name: str,
1685+
description: Optional[str] = "",
1686+
property_type: Optional[str] = None,
1687+
is_required: Optional[bool] = True,
1688+
is_array: Optional[bool] = False):
1689+
"""
1690+
Decorator for defining explicit MCP tool property metadata for a specific argument.
1691+
1692+
Example:
1693+
@app.mcp_tool_property(
1694+
arg_name="snippetname",
1695+
description="The name of the snippet.",
1696+
property_type="string",
1697+
is_required=True,
1698+
is_array=False
1699+
)
1700+
"""
1701+
def decorator(func):
1702+
# If this function is already wrapped by FunctionBuilder or similar, unwrap it
1703+
target_func = getattr(func, "_function", func)
1704+
target_func = getattr(target_func, "_func", target_func)
1705+
1706+
existing = getattr(target_func, "__mcp_tool_properties__", {})
1707+
existing[arg_name] = {
1708+
"description": description or "",
1709+
"propertyType": property_type or "string",
1710+
"isRequired": is_required,
1711+
"isArray": is_array,
1712+
}
1713+
setattr(target_func, "__mcp_tool_properties__", existing)
1714+
return func
1715+
return decorator
1716+
1717+
16631718
def dapr_service_invocation_trigger(self,
16641719
arg_name: str,
16651720
method_name: str,

azure/functions/decorators/mcp.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
3-
from typing import Optional
3+
import inspect
4+
5+
from typing import List, Optional, Union, get_origin, get_args
46
from datetime import datetime
57

68
from azure.functions.decorators.constants import (
@@ -36,3 +38,51 @@ def __init__(self,
3638
self.description = description
3739
self.tool_properties = tool_properties
3840
super().__init__(name=name, data_type=data_type)
41+
42+
def unwrap_optional(pytype: type):
43+
"""If Optional[T], return T; else return pytype unchanged."""
44+
origin = get_origin(pytype)
45+
args = get_args(pytype)
46+
if origin is Union and any(a is type(None) for a in args):
47+
non_none_args = [a for a in args if a is not type(None)]
48+
return non_none_args[0] if non_none_args else str
49+
return pytype
50+
51+
52+
def check_is_array(param_type_hint: type) -> bool:
53+
"""Return True if type is (possibly optional) list[...]"""
54+
unwrapped = unwrap_optional(param_type_hint)
55+
origin = get_origin(unwrapped)
56+
return origin in (list, List)
57+
58+
59+
def check_property_type(pytype: type, is_array: bool) -> str:
60+
"""Map Python type hints to MCP property types."""
61+
base_type = unwrap_optional(pytype)
62+
if is_array:
63+
args = get_args(base_type)
64+
inner_type = unwrap_optional(args[0]) if args else str
65+
return _TYPE_MAPPING.get(inner_type, "string")
66+
return _TYPE_MAPPING.get(base_type, "string")
67+
68+
def check_is_required(param: type, param_type_hint: type) -> bool:
69+
"""
70+
Return True when param is required, False when optional.
71+
72+
Rules:
73+
- If param has an explicit default -> not required
74+
- If annotation is Optional[T] (Union[..., None]) -> not required
75+
- Otherwise -> required
76+
"""
77+
# 1) default value present => not required
78+
if param.default is not inspect.Parameter.empty:
79+
return False
80+
81+
# 2) Optional[T] => not required
82+
origin = get_origin(param_type_hint)
83+
args = get_args(param_type_hint)
84+
if origin is Union and any(a is type(None) for a in args):
85+
return False
86+
87+
# 3) It's required
88+
return True

0 commit comments

Comments
 (0)