Skip to content

Commit 7652a86

Browse files
authored
Merge branch 'dev' into mysql-trigger
2 parents 7c15436 + d869d94 commit 7652a86

File tree

9 files changed

+185
-5
lines changed

9 files changed

+185
-5
lines changed

azure/functions/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,4 @@
102102
'BlobSource'
103103
)
104104

105-
__version__ = '1.24.0b3'
105+
__version__ = '1.24.0b4'

azure/functions/decorators/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@
4545
SEMANTIC_SEARCH = "semanticSearch"
4646
MYSQL = "mysql"
4747
MYSQL_TRIGGER = "mysqlTrigger"
48+
MCP_TOOL_TRIGGER = "mcpToolTrigger"

azure/functions/decorators/function_app.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, \
4343
semantic_search_system_prompt, \
4444
SemanticSearchInput, EmbeddingsStoreOutput
45+
from .mcp import MCPToolTrigger
4546
from .retry_policy import RetryPolicy
4647
from .function_name import FunctionName
4748
from .warmup import WarmUpTrigger
@@ -1511,6 +1512,57 @@ def decorator():
15111512

15121513
return wrap
15131514

1515+
def mcp_tool_trigger(self,
1516+
arg_name: str,
1517+
tool_name: str,
1518+
description: Optional[str] = None,
1519+
tool_properties: Optional[str] = None,
1520+
data_type: Optional[Union[DataType, str]] = None,
1521+
**kwargs) -> Callable[..., Any]:
1522+
"""
1523+
The `mcp_tool_trigger` decorator adds :class:`MCPToolTrigger` to the
1524+
:class:`FunctionBuilder` object for building a :class:`Function` object
1525+
used in the worker function indexing model.
1526+
1527+
This is equivalent to defining `MCPToolTrigger` in the `function.json`,
1528+
which enables the function to be triggered when MCP tool requests are
1529+
received by the host.
1530+
1531+
All optional fields will be given default values by the function host when
1532+
they are parsed.
1533+
1534+
Ref: https://aka.ms/remote-mcp-functions-python
1535+
1536+
:param arg_name: The name of the trigger parameter in the function code.
1537+
:param tool_name: The logical tool name exposed to the host.
1538+
:param description: Optional human-readable description of the tool.
1539+
:param tool_properties: JSON-serialized tool properties/parameters list.
1540+
:param data_type: Defines how the Functions runtime should treat the
1541+
parameter value.
1542+
:param kwargs: Keyword arguments for specifying additional binding
1543+
fields to include in the binding JSON.
1544+
1545+
:return: Decorator function.
1546+
"""
1547+
1548+
@self._configure_function_builder
1549+
def wrap(fb):
1550+
def decorator():
1551+
fb.add_trigger(
1552+
trigger=MCPToolTrigger(
1553+
name=arg_name,
1554+
tool_name=tool_name,
1555+
description=description,
1556+
tool_properties=tool_properties,
1557+
data_type=parse_singular_param_to_enum(data_type,
1558+
DataType),
1559+
**kwargs))
1560+
return fb
1561+
1562+
return decorator()
1563+
1564+
return wrap
1565+
15141566
def dapr_service_invocation_trigger(self,
15151567
arg_name: str,
15161568
method_name: str,

azure/functions/decorators/mcp.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Optional
2+
3+
from azure.functions.decorators.constants import (
4+
MCP_TOOL_TRIGGER
5+
)
6+
from azure.functions.decorators.core import Trigger, DataType
7+
8+
9+
class MCPToolTrigger(Trigger):
10+
11+
@staticmethod
12+
def get_binding_name() -> str:
13+
return MCP_TOOL_TRIGGER
14+
15+
def __init__(self,
16+
name: str,
17+
tool_name: str,
18+
description: Optional[str] = None,
19+
tool_properties: Optional[str] = None,
20+
data_type: Optional[DataType] = None,
21+
**kwargs):
22+
self.tool_name = tool_name
23+
self.description = description
24+
self.tool_properties = tool_properties
25+
super().__init__(name=name, data_type=data_type)

azure/functions/mcp.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import typing
2+
3+
from . import meta
4+
5+
6+
class MCPToolTriggerConverter(meta.InConverter, binding='mcpToolTrigger',
7+
trigger=True):
8+
9+
@classmethod
10+
def check_input_type_annotation(cls, pytype: type) -> bool:
11+
return issubclass(pytype, (str, dict, bytes))
12+
13+
@classmethod
14+
def has_implicit_output(cls) -> bool:
15+
return True
16+
17+
@classmethod
18+
def decode(cls, data: meta.Datum, *, trigger_metadata):
19+
"""
20+
Decode incoming MCP tool request data.
21+
Returns the raw data in its native format (string, dict, bytes).
22+
"""
23+
# Handle different data types appropriately
24+
if data.type == 'json':
25+
# If it's already parsed JSON, use the value directly
26+
return data.value
27+
elif data.type == 'string':
28+
# If it's a string, use it as-is
29+
return data.value
30+
elif data.type == 'bytes':
31+
return data.value
32+
else:
33+
# Fallback to python_value for other types
34+
return data.python_value if hasattr(data, 'python_value') else data.value
35+
36+
@classmethod
37+
def encode(cls, obj: typing.Any, *, expected_type: typing.Optional[type] = None):
38+
"""
39+
Encode the return value from MCP tool functions.
40+
MCP tools typically return string responses.
41+
"""
42+
if obj is None:
43+
return meta.Datum(type='string', value='')
44+
elif isinstance(obj, str):
45+
return meta.Datum(type='string', value=obj)
46+
elif isinstance(obj, (bytes, bytearray)):
47+
return meta.Datum(type='bytes', value=bytes(obj))
48+
else:
49+
# Convert other types to string
50+
return meta.Datum(type='string', value=str(obj))

eng/templates/jobs/build.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ jobs:
1616
PYTHON_VERSION: '3.11'
1717
Python312:
1818
PYTHON_VERSION: '3.12'
19-
19+
Python313:
20+
PYTHON_VERSION: '3.13'
21+
2022
steps:
2123
- task: UsePythonVersion@0
2224
inputs:

eng/templates/jobs/ci-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jobs:
1616
PYTHON_VERSION: '3.11'
1717
python-312:
1818
PYTHON_VERSION: '3.12'
19+
python-313:
20+
PYTHON_VERSION: '3.13'
21+
1922
steps:
2023
- task: UsePythonVersion@0
2124
inputs:

eng/templates/official/jobs/publish-release.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
# Create release branch release/X.Y.Z
3030
Write-Host "Creating release branch release/$newLibraryVersion"
3131
git push --repo="https://$githubToken@github.com/Azure/azure-functions-python-library.git"
32+
3233
} else {
3334
Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.5.0)"
3435
exit -1
@@ -144,11 +145,11 @@ jobs:
144145
145146
# Modify SDK Version in pyproject.toml
146147
Write-Host "Replacing SDK version in worker's pyproject.toml"
147-
((Get-Content pyproject.toml) -replace '"azure-functions==(\d)+.(\d)+.*"','"azure-functions==$(NewLibraryVersion)"' -join "`n") + "`n" | Set-Content -NoNewline pyproject.toml
148-
148+
((Get-Content workers/pyproject.toml) -replace '"azure-functions==(\d)+.(\d)+.*"','"azure-functions==$(NewLibraryVersion)"' -join "`n") + "`n" | Set-Content -NoNewline workers/pyproject.toml
149+
149150
# Commit Python Version
150151
Write-Host "Pushing $newBranch to azure-functions-python-worker repo"
151-
git add pyproject.toml
152+
git add workers/pyproject.toml
152153
git commit -m "Update Python SDK Version to $newLibraryVersion"
153154
git push origin $newBranch
154155

tests/decorators/test_mcp.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import unittest
2+
3+
from azure.functions import DataType
4+
from azure.functions.decorators.core import BindingDirection
5+
from azure.functions.decorators.mcp import MCPToolTrigger
6+
from azure.functions.mcp import MCPToolTriggerConverter
7+
from azure.functions.meta import Datum
8+
9+
10+
class TestMCP(unittest.TestCase):
11+
def test_mcp_tool_trigger_valid_creation(self):
12+
trigger = MCPToolTrigger(
13+
name="context",
14+
tool_name="hello",
15+
description="Hello world.",
16+
tool_properties="[]",
17+
data_type=DataType.UNDEFINED,
18+
dummy_field="dummy",
19+
)
20+
self.assertEqual(trigger.get_binding_name(), "mcpToolTrigger")
21+
self.assertEqual(
22+
trigger.get_dict_repr(),
23+
{
24+
"name": "context",
25+
"toolName": "hello",
26+
"description": "Hello world.",
27+
"toolProperties": "[]",
28+
"type": "mcpToolTrigger",
29+
"dataType": DataType.UNDEFINED,
30+
"dummyField": "dummy",
31+
"direction": BindingDirection.IN,
32+
},
33+
)
34+
35+
def test_trigger_converter(self):
36+
# Test with string data
37+
datum = Datum(value='{"arguments":{}}', type='string')
38+
result = MCPToolTriggerConverter.decode(datum, trigger_metadata={})
39+
self.assertEqual(result, '{"arguments":{}}')
40+
self.assertIsInstance(result, str)
41+
42+
# Test with json data
43+
datum_json = Datum(value={"arguments": {}}, type='json')
44+
result_json = MCPToolTriggerConverter.decode(datum_json, trigger_metadata={})
45+
self.assertEqual(result_json, {"arguments": {}})
46+
self.assertIsInstance(result_json, dict)

0 commit comments

Comments
 (0)