diff --git a/README.md b/README.md index 9f7c4a2..c60e1f0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/infra/app/api.bicep b/infra/app/api.bicep index 0b1e126..cfcc401 100644 --- a/infra/app/api.bicep +++ b/infra/app/api.bicep @@ -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 diff --git a/src/function_app.py b/src/function_app.py index c0bb780..9d468cf 100644 --- a/src/function_app.py +++ b/src/function_app.py @@ -1,4 +1,3 @@ -import json import logging import azure.functions as func @@ -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}" diff --git a/src/local.settings.json b/src/local.settings.json index 504bb06..d96820c 100644 --- a/src/local.settings.json +++ b/src/local.settings.json @@ -2,6 +2,7 @@ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "UseDevelopmentStorage=true" + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1" } } \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt index 25671e4..fb8b155 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -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