Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 29 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,84 +274,50 @@ azd deploy

The function code for the `get_snippet` and `save_snippet` endpoints are defined in the Python files in the `src` directory. The MCP function annotations expose these functions as MCP Server tools.

Here's the actual code from the function_app.py file:
Here's the actual code from the function_app.py file using the new `@app.mcp_tool()` decorators (requires `azure-functions>=1.25.0b2`):

```python

@app.generic_trigger(arg_name="context", type="mcpToolTrigger", toolName="hello",
description="Hello world.",
toolProperties="[]")
def hello_mcp(context) -> None:
"""
A simple function that returns a greeting message.

Args:
context: The trigger context (not used in this function).

Returns:
str: A greeting message.
"""
@app.mcp_tool()
def hello_mcp() -> str:
"""Hello world."""
return "Hello I am MCPTool!"


@app.generic_trigger(
arg_name="context",
type="mcpToolTrigger",
toolName="getsnippet",
description="Retrieve a snippet by name.",
toolProperties=tool_properties_get_snippets_json
)
@app.generic_input_binding(
arg_name="file",
type="blob",
connection="AzureWebJobsStorage",
path=_BLOB_PATH
)
def get_snippet(file: func.InputStream, context) -> str:
"""
Retrieves a snippet by name from Azure Blob Storage.

Args:
file (func.InputStream): The input binding to read the snippet from Azure Blob Storage.
context: The trigger context containing the input arguments.

Returns:
str: The content of the snippet or an error message.
"""
@app.mcp_tool()
@app.mcp_tool_property(arg_name="snippetname", description="The name of the snippet.")
@app.blob_input(arg_name="file", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def get_snippet(file: func.InputStream, snippetname: str) -> str:
"""Retrieve a snippet by name from Azure Blob Storage."""
snippet_content = file.read().decode("utf-8")
logging.info(f"Retrieved snippet: {snippet_content}")
return snippet_content


@app.generic_trigger(
arg_name="context",
type="mcpToolTrigger",
toolName="savesnippet",
description="Save a snippet with a name.",
toolProperties=tool_properties_save_snippets_json
)
@app.generic_output_binding(
arg_name="file",
type="blob",
connection="AzureWebJobsStorage",
path=_BLOB_PATH
)
def save_snippet(file: func.Out[str], context) -> str:
content = json.loads(context)
snippet_name_from_args = content["arguments"][_SNIPPET_NAME_PROPERTY_NAME]
snippet_content_from_args = content["arguments"][_SNIPPET_PROPERTY_NAME]

if not snippet_name_from_args:
@app.mcp_tool()
@app.mcp_tool_property(arg_name="snippetname", description="The name of the snippet.")
@app.mcp_tool_property(arg_name="snippet", description="The content of the snippet.")
@app.blob_output(arg_name="file", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def save_snippet(file: func.Out[str], snippetname: str, snippet: str) -> str:
"""Save a snippet to Azure Blob Storage."""
if not snippetname:
return "No snippet name provided"

if not snippet_content_from_args:
if not snippet:
return "No snippet content provided"
file.set(snippet_content_from_args)
logging.info(f"Saved snippet: {snippet_content_from_args}")
return f"Snippet '{snippet_content_from_args}' saved successfully"

file.set(snippet)
logging.info(f"Saved snippet: {snippet}")
return f"Snippet '{snippetname}' saved successfully with content: {snippet}"
```

Key features of the new MCP decorators:
- `@app.mcp_tool()` replaces `@app.generic_trigger(type="mcpToolTrigger", ...)`
- Tool properties are inferred from function signatures and type hints
- `@app.mcp_tool_property()` explicitly defines tool property descriptions
- `@app.blob_input()` and `@app.blob_output()` replace generic bindings
- No manual JSON parsing required - arguments arrive as function parameters
- Tool descriptions come from function docstrings

Note that the `host.json` file also includes a reference to the experimental bundle, which is required for apps using this feature:

```json
Expand Down
3 changes: 3 additions & 0 deletions infra/app/api.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ var baseAppSettings = {
// Application Insights settings are always included
APPLICATIONINSIGHTS_AUTHENTICATION_STRING: applicationInsightsIdentity
APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString

// Required for MCP decorators (azure-functions 1.25.0b2+)
PYTHON_ISOLATE_WORKER_DEPENDENCIES: '1'
}

// Dynamically build storage endpoint settings based on feature flags
Expand Down
102 changes: 19 additions & 83 deletions src/function_app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import logging

import azure.functions as func
Expand All @@ -11,96 +10,33 @@
_BLOB_PATH = "snippets/{mcptoolargs." + _SNIPPET_NAME_PROPERTY_NAME + "}.json"


class ToolProperty:
def __init__(self, property_name: str, property_type: str, description: str):
self.propertyName = property_name
self.propertyType = property_type
self.description = description

def to_dict(self):
return {
"propertyName": self.propertyName,
"propertyType": self.propertyType,
"description": self.description,
}


# Define the tool properties using the ToolProperty class
tool_properties_save_snippets_object = [
ToolProperty(_SNIPPET_NAME_PROPERTY_NAME, "string", "The name of the snippet."),
ToolProperty(_SNIPPET_PROPERTY_NAME, "string", "The content of the snippet."),
]

tool_properties_get_snippets_object = [ToolProperty(_SNIPPET_NAME_PROPERTY_NAME, "string", "The name of the snippet.")]

# Convert the tool properties to JSON
tool_properties_save_snippets_json = json.dumps([prop.to_dict() for prop in tool_properties_save_snippets_object])
tool_properties_get_snippets_json = json.dumps([prop.to_dict() for prop in tool_properties_get_snippets_object])


@app.generic_trigger(
arg_name="context",
type="mcpToolTrigger",
toolName="hello_mcp",
description="Hello world.",
toolProperties="[]",
)
def hello_mcp(context) -> None:
"""
A simple function that returns a greeting message.

Args:
context: The trigger context (not used in this function).

Returns:
str: A greeting message.
"""
@app.mcp_tool()
def hello_mcp() -> str:
"""Hello world."""
return "Hello I am MCPTool!"


@app.generic_trigger(
arg_name="context",
type="mcpToolTrigger",
toolName="get_snippet",
description="Retrieve a snippet by name.",
toolProperties=tool_properties_get_snippets_json,
)
@app.generic_input_binding(arg_name="file", type="blob", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def get_snippet(file: func.InputStream, context) -> str:
"""
Retrieves a snippet by name from Azure Blob Storage.

Args:
file (func.InputStream): The input binding to read the snippet from Azure Blob Storage.
context: The trigger context containing the input arguments.

Returns:
str: The content of the snippet or an error message.
"""
@app.mcp_tool()
@app.mcp_tool_property(arg_name="snippetname", description="The name of the snippet.")
@app.blob_input(arg_name="file", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def get_snippet(file: func.InputStream, snippetname: str) -> str:
"""Retrieve a snippet by name from Azure Blob Storage."""
snippet_content = file.read().decode("utf-8")
logging.info(f"Retrieved snippet: {snippet_content}")
return snippet_content


@app.generic_trigger(
arg_name="context",
type="mcpToolTrigger",
toolName="save_snippet",
description="Save a snippet with a name.",
toolProperties=tool_properties_save_snippets_json,
)
@app.generic_output_binding(arg_name="file", type="blob", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def save_snippet(file: func.Out[str], context) -> str:
content = json.loads(context)
snippet_name_from_args = content["arguments"][_SNIPPET_NAME_PROPERTY_NAME]
snippet_content_from_args = content["arguments"][_SNIPPET_PROPERTY_NAME]

if not snippet_name_from_args:
@app.mcp_tool()
@app.mcp_tool_property(arg_name="snippetname", description="The name of the snippet.")
@app.mcp_tool_property(arg_name="snippet", description="The content of the snippet.")
@app.blob_output(arg_name="file", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def save_snippet(file: func.Out[str], snippetname: str, snippet: str) -> str:
"""Save a snippet to Azure Blob Storage."""
if not snippetname:
return "No snippet name provided"

if not snippet_content_from_args:
if not snippet:
return "No snippet content provided"

file.set(snippet_content_from_args)
logging.info(f"Saved snippet: {snippet_content_from_args}")
return f"Snippet '{snippet_content_from_args}' saved successfully"
file.set(snippet)
logging.info(f"Saved snippet: {snippet}")
return f"Snippet '{snippetname}' saved successfully with content: {snippet}"
3 changes: 2 additions & 1 deletion src/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "python",
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1"
}
}
2 changes: 1 addition & 1 deletion src/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# The Python Worker is managed by the Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions
azure-functions==1.25.0b2