Skip to content

Commit 857ff63

Browse files
committed
add tests
1 parent 8476f18 commit 857ff63

File tree

5 files changed

+167
-2
lines changed

5 files changed

+167
-2
lines changed

azure/functions/decorators/function_app.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,6 @@ def _validate_function(self,
275275
trigger = self._function.get_trigger()
276276
if trigger is None:
277277
raise ValueError(
278-
f"This is the function: {self._function}"
279278
f"Function {function_name} does not have a trigger. A valid "
280279
f"function must have one and only one trigger registered.")
281280

azure/functions/decorators/mcp.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
13
from typing import Optional
24
from typing import Any, Tuple, get_args, get_origin, Annotated
35

azure/functions/mcp.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
13
import typing
24

35
from . import meta

tests/decorators/test_mcp.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import typing
14
import unittest
25

3-
from azure.functions import DataType
6+
import azure.functions as func
7+
from azure.functions import DataType, MCPToolContext
48
from azure.functions.decorators.core import BindingDirection
59
from azure.functions.decorators.mcp import MCPToolTrigger
610
from azure.functions.mcp import MCPToolTriggerConverter
@@ -44,3 +48,85 @@ def test_trigger_converter(self):
4448
result_json = MCPToolTriggerConverter.decode(datum_json, trigger_metadata={})
4549
self.assertEqual(result_json, {"arguments": {}})
4650
self.assertIsInstance(result_json, dict)
51+
52+
53+
class TestMcpToolDecorator(unittest.TestCase):
54+
def setUp(self):
55+
self.app = func.FunctionApp()
56+
57+
def tearDown(self):
58+
self.app = None
59+
60+
def test_simple_signature(self):
61+
@self.app.mcp_tool()
62+
def add_numbers(a: int, b: int) -> int:
63+
"""Add two numbers."""
64+
return a + b
65+
66+
trigger = add_numbers._function._bindings[0]
67+
self.assertEqual(trigger.description, "Add two numbers.")
68+
self.assertEqual(trigger.name, "context")
69+
self.assertEqual(trigger.tool_name, "add_numbers")
70+
self.assertEqual(trigger.tool_properties,
71+
'[{"propertyName": "a", '
72+
'"propertyType": "integer", '
73+
'"description": "The a parameter."}, '
74+
'{"propertyName": "b", "propertyType": "integer", '
75+
'"description": "The b parameter."}]')
76+
77+
def test_with_binding_argument(self):
78+
@self.app.mcp_tool()
79+
def save_snippet(file, snippetname: str, snippet: str):
80+
"""Save snippet."""
81+
return f"Saved {snippetname}"
82+
83+
trigger = save_snippet._function._bindings[0]
84+
self.assertEqual(trigger.description, "Save snippet.")
85+
self.assertEqual(trigger.name, "context")
86+
self.assertEqual(trigger.tool_name, "save_snippet")
87+
self.assertEqual(trigger.tool_properties,
88+
'[{"propertyName": "file", '
89+
'"propertyType": "string", '
90+
'"description": "The file parameter."}, '
91+
'{"propertyName": "snippetname", '
92+
'"propertyType": "string", '
93+
'"description": "The snippetname parameter."}, '
94+
'{"propertyName": "snippet", '
95+
'"propertyType": "string", '
96+
'"description": "The snippet parameter."}]')
97+
98+
def test_with_context_argument(self):
99+
@self.app.mcp_tool()
100+
def process_data(data: str, context: MCPToolContext):
101+
"""Process data with context."""
102+
return f"Processed {data}"
103+
104+
trigger = process_data._function._bindings[0]
105+
self.assertEqual(trigger.description, "Process data with context.")
106+
self.assertEqual(trigger.name, "context")
107+
self.assertEqual(trigger.tool_name, "process_data")
108+
self.assertEqual(trigger.tool_properties,
109+
'[{"propertyName": "data", '
110+
'"propertyType": "string", '
111+
'"description": "The data parameter."}]')
112+
113+
def test_with_annotated(self):
114+
@self.app.mcp_tool()
115+
def add_numbers(
116+
a: typing.Annotated[int, "First number"],
117+
b: typing.Annotated[int, "Second number"]
118+
) -> str:
119+
"""Add two integers."""
120+
return str(a + b)
121+
122+
trigger = add_numbers._function._bindings[0]
123+
self.assertEqual(trigger.description, "Add two integers.")
124+
self.assertEqual(trigger.name, "context")
125+
self.assertEqual(trigger.tool_name, "add_numbers")
126+
self.assertEqual(trigger.tool_properties,
127+
'[{"propertyName": "a", '
128+
'"propertyType": "integer", '
129+
'"description": "First number"}, '
130+
'{"propertyName": "b", '
131+
'"propertyType": "integer", '
132+
'"description": "Second number"}]')

tests/test_mcp.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import unittest
4+
import azure.functions as func
5+
from azure.functions.meta import Datum
6+
from azure.functions.mcp import MCPToolTriggerConverter
7+
8+
9+
class TestMCPToolTriggerConverter(unittest.TestCase):
10+
"""Unit tests for MCPToolTriggerConverter"""
11+
12+
def test_check_input_type_annotation_valid_types(self):
13+
self.assertTrue(MCPToolTriggerConverter.check_input_type_annotation(str))
14+
self.assertTrue(MCPToolTriggerConverter.check_input_type_annotation(dict))
15+
self.assertTrue(MCPToolTriggerConverter.check_input_type_annotation(bytes))
16+
self.assertTrue(MCPToolTriggerConverter.check_input_type_annotation(func.MCPToolContext))
17+
18+
def test_check_input_type_annotation_invalid_type(self):
19+
with self.assertRaises(TypeError):
20+
MCPToolTriggerConverter.check_input_type_annotation(123) # not a type
21+
22+
class Dummy:
23+
pass
24+
self.assertFalse(MCPToolTriggerConverter.check_input_type_annotation(Dummy))
25+
26+
def test_has_implicit_output(self):
27+
self.assertTrue(MCPToolTriggerConverter.has_implicit_output())
28+
29+
def test_decode_json(self):
30+
data = Datum(type='json', value={'foo': 'bar'})
31+
result = MCPToolTriggerConverter.decode(data, trigger_metadata={})
32+
self.assertEqual(result, {'foo': 'bar'})
33+
34+
def test_decode_string(self):
35+
data = Datum(type='string', value='hello')
36+
result = MCPToolTriggerConverter.decode(data, trigger_metadata={})
37+
self.assertEqual(result, 'hello')
38+
39+
def test_decode_bytes(self):
40+
data = Datum(type='bytes', value=b'data')
41+
result = MCPToolTriggerConverter.decode(data, trigger_metadata={})
42+
self.assertEqual(result, b'data')
43+
44+
def test_decode_other_without_python_value(self):
45+
data = Datum(type='other', value='fallback')
46+
result = MCPToolTriggerConverter.decode(data, trigger_metadata={})
47+
self.assertEqual(result, 'fallback')
48+
49+
def test_encode_none(self):
50+
result = MCPToolTriggerConverter.encode(None)
51+
self.assertEqual(result.type, 'string')
52+
self.assertEqual(result.value, '')
53+
54+
def test_encode_string(self):
55+
result = MCPToolTriggerConverter.encode('hello')
56+
self.assertEqual(result.type, 'string')
57+
self.assertEqual(result.value, 'hello')
58+
59+
def test_encode_bytes(self):
60+
result = MCPToolTriggerConverter.encode(b'\x00\x01')
61+
self.assertEqual(result.type, 'bytes')
62+
self.assertEqual(result.value, b'\x00\x01')
63+
64+
def test_encode_bytearray(self):
65+
result = MCPToolTriggerConverter.encode(bytearray(b'\x01\x02'))
66+
self.assertEqual(result.type, 'bytes')
67+
self.assertEqual(result.value, b'\x01\x02')
68+
69+
def test_encode_other_type(self):
70+
result = MCPToolTriggerConverter.encode(42)
71+
self.assertEqual(result.type, 'string')
72+
self.assertEqual(result.value, '42')
73+
74+
result = MCPToolTriggerConverter.encode({'a': 1})
75+
self.assertEqual(result.type, 'string')
76+
self.assertIn("'a'", result.value)

0 commit comments

Comments
 (0)