Skip to content

Commit bc050a1

Browse files
committed
simplified mcp decorator
1 parent cbe1fb9 commit bc050a1

4 files changed

Lines changed: 103 additions & 5 deletions

File tree

azure/functions/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
DecoratorApi, DataType, AuthLevel,
1111
Cardinality, AccessRights, HttpMethod,
1212
AsgiFunctionApp, WsgiFunctionApp,
13-
ExternalHttpFunctionApp, BlobSource)
13+
ExternalHttpFunctionApp, BlobSource, MCPToolContext)
1414
from ._durable_functions import OrchestrationContext, EntityContext
1515
from .decorators.function_app import (FunctionRegister, TriggerApi,
1616
BindingApi, SettingsApi)
@@ -99,7 +99,8 @@
9999
'Cardinality',
100100
'AccessRights',
101101
'HttpMethod',
102-
'BlobSource'
102+
'BlobSource',
103+
'MCPToolContext'
103104
)
104105

105106
__version__ = '1.25.0b1'

azure/functions/decorators/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .function_app import FunctionApp, Function, DecoratorApi, DataType, \
55
AuthLevel, Blueprint, ExternalHttpFunctionApp, AsgiFunctionApp, \
66
WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi, \
7-
SettingsApi, BlobSource
7+
SettingsApi, BlobSource, MCPToolContext
88
from .http import HttpMethod
99

1010
__all__ = [
@@ -24,5 +24,6 @@
2424
'Cardinality',
2525
'AccessRights',
2626
'HttpMethod',
27-
'BlobSource'
27+
'BlobSource',
28+
'MCPToolContext'
2829
]

azure/functions/decorators/function_app.py

Lines changed: 74 additions & 1 deletion
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 inspect
56
import json
67
import logging
78
from abc import ABC
@@ -42,7 +43,7 @@
4243
AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, \
4344
semantic_search_system_prompt, \
4445
SemanticSearchInput, EmbeddingsStoreOutput
45-
from .mcp import MCPToolTrigger
46+
from .mcp import MCPToolTrigger, MCPToolContext, _TYPE_MAPPING, _extract_type_and_description
4647
from .retry_policy import RetryPolicy
4748
from .function_name import FunctionName
4849
from .warmup import WarmUpTrigger
@@ -462,6 +463,78 @@ def auth_level(self) -> AuthLevel:
462463

463464
class TriggerApi(DecoratorApi, ABC):
464465
"""Interface to extend for using existing trigger decorator functions."""
466+
def mcp_tool(self) -> Callable[[Callable], Callable]:
467+
"""
468+
Decorator to register an MCP tool function.
469+
470+
Automatically:
471+
- Infers tool name from function name
472+
- Extracts first line of docstring as description
473+
- Extracts parameters and types for tool properties
474+
- Handles MCPToolContext injection
475+
"""
476+
def decorator(target_func: Callable) -> Callable:
477+
sig = inspect.signature(target_func)
478+
tool_name = target_func.__name__
479+
description = (target_func.__doc__ or "").strip().split("\n")[0]
480+
481+
# Build tool properties metadata
482+
tool_properties = []
483+
for param_name, param in sig.parameters.items():
484+
param_type_hint = param.annotation if param.annotation != inspect.Parameter.empty else str
485+
actual_type, param_description = _extract_type_and_description(param_name, param_type_hint)
486+
if actual_type is MCPToolContext:
487+
continue
488+
property_type = _TYPE_MAPPING.get(actual_type, "string")
489+
tool_properties.append({
490+
"propertyName": param_name,
491+
"propertyType": property_type,
492+
"description": param_description,
493+
})
494+
495+
tool_properties_json = json.dumps(tool_properties)
496+
497+
# Wrapper function for MCP trigger
498+
def wrapper(context: str) -> str:
499+
try:
500+
content = json.loads(context)
501+
arguments = content.get("arguments", {})
502+
kwargs = {}
503+
504+
for param_name, param in sig.parameters.items():
505+
param_type_hint = param.annotation if param.annotation != inspect.Parameter.empty else str
506+
actual_type, _ = _extract_type_and_description(param_name, param_type_hint)
507+
508+
if actual_type is MCPToolContext:
509+
kwargs[param_name] = content
510+
elif param_name in arguments:
511+
kwargs[param_name] = arguments[param_name]
512+
else:
513+
return f"Error: Missing required parameter '{param_name}' for '{tool_name}'"
514+
515+
result = target_func(**kwargs)
516+
return str(result)
517+
518+
except Exception as e:
519+
return f"Error executing function '{tool_name}': {str(e)}"
520+
521+
wrapper.__name__ = target_func.__name__
522+
wrapper.__doc__ = target_func.__doc__
523+
524+
# Use the existing FunctionRegister mechanism to add the trigger
525+
fb = self._configure_function_builder(lambda fb: fb)(wrapper)
526+
fb.add_trigger(
527+
trigger=MCPToolTrigger(
528+
name="context",
529+
tool_name=tool_name,
530+
description=description,
531+
tool_properties=tool_properties_json
532+
)
533+
)
534+
535+
return fb
536+
537+
return decorator
465538

466539
def route(self,
467540
route: Optional[str] = None,

azure/functions/decorators/mcp.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
from typing import Optional
2+
from typing import Any, Dict, Tuple, get_args, get_origin, Annotated
23

34
from azure.functions.decorators.constants import (
45
MCP_TOOL_TRIGGER
56
)
67
from azure.functions.decorators.core import Trigger, DataType
78

9+
# Mapping Python types to MCP property types
10+
_TYPE_MAPPING = {
11+
int: "integer",
12+
float: "number",
13+
str: "string",
14+
bool: "boolean",
15+
}
816

917
class MCPToolTrigger(Trigger):
1018

@@ -23,3 +31,18 @@ def __init__(self,
2331
self.description = description
2432
self.tool_properties = tool_properties
2533
super().__init__(name=name, data_type=data_type)
34+
35+
# MCP-specific context object
36+
class MCPToolContext(Dict[str, Any]):
37+
"""Injected context object for MCP tool triggers."""
38+
pass
39+
40+
# Helper to extract actual type and description from Annotated types
41+
def _extract_type_and_description(param_name: str, type_hint: Any) -> Tuple[Any, str]:
42+
if get_origin(type_hint) is Annotated:
43+
args = get_args(type_hint)
44+
actual_type = args[0]
45+
# Use first string annotation as description if present
46+
param_description = next((a for a in args[1:] if isinstance(a, str)), f"The {param_name} parameter.")
47+
return actual_type, param_description
48+
return type_hint, f"The {param_name} parameter."

0 commit comments

Comments
 (0)