Skip to content

Commit 7f29e50

Browse files
committed
feedback
1 parent 1059e5c commit 7f29e50

File tree

3 files changed

+93
-42
lines changed

3 files changed

+93
-42
lines changed

azure/functions/decorators/function_app.py

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import inspect
77
import json
88
import logging
9+
import textwrap
10+
911
from abc import ABC
1012
from datetime import time
1113
from typing import Any, Callable, Dict, List, Optional, Union, \
@@ -45,7 +47,7 @@
4547
AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, \
4648
semantic_search_system_prompt, \
4749
SemanticSearchInput, EmbeddingsStoreOutput
48-
from .mcp import MCPToolTrigger, check_property_type, check_is_array, check_is_required
50+
from .mcp import MCPToolTrigger, build_property_metadata
4951
from .retry_policy import RetryPolicy
5052
from .function_name import FunctionName
5153
from .warmup import WarmUpTrigger
@@ -1595,43 +1597,17 @@ def decorator(fb: FunctionBuilder) -> FunctionBuilder:
15951597

15961598
# Parse tool name and description from function signature
15971599
tool_name = target_func.__name__
1598-
description = (target_func.__doc__ or "").strip().split("\n")[0]
1600+
raw_doc = target_func.__doc__ or ""
1601+
description = textwrap.dedent(raw_doc).strip()
15991602

16001603
# Identify arguments that are already bound (bindings)
16011604
bound_param_names = {b.name for b in getattr(fb._function, "_bindings", [])}
16021605
skip_param_names = bound_param_names
16031606

16041607
# Build tool properties
1605-
tool_properties = []
1606-
for param_name, param in sig.parameters.items():
1607-
if param_name in skip_param_names:
1608-
continue
1609-
param_type_hint = param.annotation if param.annotation != inspect.Parameter.empty else str # noqa
1610-
1611-
if param_type_hint is MCPToolContext:
1612-
continue
1613-
1614-
# Inferred defaults
1615-
is_required = check_is_required(param, param_type_hint)
1616-
is_array = check_is_array(param_type_hint)
1617-
property_type = check_property_type(param_type_hint, is_array)
1618-
1619-
property_data = {
1620-
"propertyName": param_name,
1621-
"propertyType": property_type,
1622-
"description": "",
1623-
"isArray": is_array,
1624-
"isRequired": is_required
1625-
}
1626-
1627-
# Merge in any explicit overrides
1628-
if param_name in explicit_properties:
1629-
overrides = explicit_properties[param_name]
1630-
for key, value in overrides.items():
1631-
if value is not None:
1632-
property_data[key] = value
1633-
1634-
tool_properties.append(property_data)
1608+
tool_properties = build_property_metadata(sig=sig,
1609+
skip_param_names=skip_param_names,
1610+
explicit_properties=explicit_properties)
16351611

16361612
tool_properties_json = json.dumps(tool_properties)
16371613

@@ -1684,15 +1660,15 @@ def mcp_tool_property(self, arg_name: str,
16841660
description: Optional[str] = None,
16851661
property_type: Optional[McpPropertyType] = None,
16861662
is_required: Optional[bool] = True,
1687-
is_array: Optional[bool] = False):
1663+
as_array: Optional[bool] = False):
16881664
"""
16891665
Decorator for defining explicit MCP tool property metadata for a specific argument.
16901666
16911667
:param arg_name: The name of the argument.
16921668
:param description: The description of the argument.
16931669
:param property_type: The type of the argument.
16941670
:param is_required: If the argument is required or not.
1695-
:param is_array: If the argument is array or not.
1671+
:param as_array: If the argument should be passed as an array or not.
16961672
16971673
:return: Decorator function.
16981674
@@ -1702,7 +1678,7 @@ def mcp_tool_property(self, arg_name: str,
17021678
description="The name of the snippet.",
17031679
property_type=func.McpPropertyType.STRING,
17041680
is_required=True,
1705-
is_array=False
1681+
as_array=False
17061682
)
17071683
"""
17081684
def decorator(func):
@@ -1715,7 +1691,7 @@ def decorator(func):
17151691
"description": description,
17161692
"propertyType": property_type.value if property_type else None, # Get enum value
17171693
"isRequired": is_required,
1718-
"isArray": is_array,
1694+
"isArray": as_array,
17191695
}
17201696
setattr(target_func, "__mcp_tool_properties__", existing)
17211697
return func

azure/functions/decorators/mcp.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import List, Optional, Union, get_origin, get_args
66
from datetime import datetime
77

8+
from ..mcp import MCPToolContext
89
from azure.functions.decorators.constants import (
910
MCP_TOOL_TRIGGER
1011
)
@@ -50,19 +51,19 @@ def unwrap_optional(pytype: type):
5051
return pytype
5152

5253

53-
def check_is_array(param_type_hint: type) -> bool:
54+
def check_as_array(param_type_hint: type) -> bool:
5455
"""Return True if type is (possibly optional) list[...]"""
5556
unwrapped = unwrap_optional(param_type_hint)
5657
origin = get_origin(unwrapped)
5758
return origin in (list, List)
5859

5960

60-
def check_property_type(pytype: type, is_array: bool) -> str:
61+
def check_property_type(pytype: type, as_array: bool) -> str:
6162
"""Map Python type hints to MCP property types."""
6263
if isinstance(pytype, McpPropertyType):
6364
return pytype.value
6465
base_type = unwrap_optional(pytype)
65-
if is_array:
66+
if as_array:
6667
args = get_args(base_type)
6768
inner_type = unwrap_optional(args[0]) if args else str
6869
return _TYPE_MAPPING.get(inner_type, "string")
@@ -90,3 +91,39 @@ def check_is_required(param: type, param_type_hint: type) -> bool:
9091

9192
# 3) It's required
9293
return True
94+
95+
96+
def build_property_metadata(sig,
97+
skip_param_names: List[str],
98+
explicit_properties: dict) -> List[dict]:
99+
tool_properties = []
100+
for param_name, param in sig.parameters.items():
101+
if param_name in skip_param_names:
102+
continue
103+
param_type_hint = param.annotation if param.annotation != inspect.Parameter.empty else str # noqa
104+
105+
if param_type_hint is MCPToolContext:
106+
continue
107+
108+
# Inferred defaults
109+
is_required = check_is_required(param, param_type_hint)
110+
as_array = check_as_array(param_type_hint)
111+
property_type = check_property_type(param_type_hint, as_array)
112+
113+
property_data = {
114+
"propertyName": param_name,
115+
"propertyType": property_type,
116+
"description": "",
117+
"isArray": as_array,
118+
"isRequired": is_required
119+
}
120+
121+
# Merge in any explicit overrides
122+
if param_name in explicit_properties:
123+
overrides = explicit_properties[param_name]
124+
for key, value in overrides.items():
125+
if value is not None:
126+
property_data[key] = value
127+
128+
tool_properties.append(property_data)
129+
return tool_properties

tests/decorators/test_mcp.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,44 @@ def add_numbers(a: int, b: int) -> int:
7979
'"isArray": false, '
8080
'"isRequired": true}]')
8181

82+
def test_long_pydocs(self):
83+
@self.app.mcp_tool()
84+
def add_numbers(a: int, b: int) -> int:
85+
"""
86+
Add two numbers.
87+
88+
Args:
89+
a (int): The first number to add.
90+
b (int): The second number to add.
91+
92+
Returns:
93+
int: The sum of the two numbers.
94+
"""
95+
return a + b
96+
97+
trigger = add_numbers._function._bindings[0]
98+
self.assertEqual(trigger.description, '''Add two numbers.
99+
100+
Args:
101+
a (int): The first number to add.
102+
b (int): The second number to add.
103+
104+
Returns:
105+
int: The sum of the two numbers.''')
106+
self.assertEqual(trigger.name, "context")
107+
self.assertEqual(trigger.tool_name, "add_numbers")
108+
self.assertEqual(trigger.tool_properties,
109+
'[{"propertyName": "a", '
110+
'"propertyType": "integer", '
111+
'"description": "", '
112+
'"isArray": false, '
113+
'"isRequired": true}, '
114+
'{"propertyName": "b", '
115+
'"propertyType": "integer", '
116+
'"description": "", '
117+
'"isArray": false, '
118+
'"isRequired": true}]')
119+
82120
def test_simple_signature_defaults(self):
83121
@self.app.mcp_tool()
84122
def add_numbers(a, b):
@@ -187,7 +225,7 @@ def add_numbers(a: int = 0) -> int:
187225
'"isArray": false, '
188226
'"isRequired": false}]')
189227

190-
def test_is_array(self):
228+
def test_as_array(self):
191229
@self.app.mcp_tool()
192230
def add_numbers(a: typing.List[int]) -> typing.List[int]:
193231
"""Add two numbers."""
@@ -204,7 +242,7 @@ def add_numbers(a: typing.List[int]) -> typing.List[int]:
204242
'"isArray": true, '
205243
'"isRequired": true}]')
206244

207-
def test_is_array_pep(self):
245+
def test_as_array_pep(self):
208246
@self.app.mcp_tool()
209247
def add_numbers(a: list[int]) -> list[int]:
210248
"""Add two numbers."""
@@ -244,7 +282,7 @@ def test_mcp_property_input_all_props(self):
244282
description="The first number",
245283
property_type=func.McpPropertyType.INTEGER,
246284
is_required=False,
247-
is_array=True)
285+
as_array=True)
248286
def add_numbers(a, b: int) -> int:
249287
"""Add two numbers."""
250288
return a + b

0 commit comments

Comments
 (0)