From 3675afccc9ec91647a4c19c8ea7d4ee1632f7d60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:36:27 +0000 Subject: [PATCH 1/5] Initial plan From a305ee40523b7a267507e2a544e94219a64a34e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:38:46 +0000 Subject: [PATCH 2/5] Migrate to new MCP decorators (azure-functions 1.25.0b2) Co-authored-by: vrdmr <1047040+vrdmr@users.noreply.github.com> --- README.md | 92 ++++++++++++------------------------ infra/app/api.bicep | 3 ++ src/function_app.py | 102 ++++++++-------------------------------- src/local.settings.json | 3 +- src/requirements.txt | 2 +- 5 files changed, 54 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index 9f7c4a2..bf82117 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: {snippetname}") + 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..ae52559 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: {snippetname}") + 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 From 55549486a1a325c9c1f5cfe56bf1a01ca104c732 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:40:31 +0000 Subject: [PATCH 3/5] Fix formatting in function_app.py Co-authored-by: vrdmr <1047040+vrdmr@users.noreply.github.com> --- src/function_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/function_app.py b/src/function_app.py index ae52559..e2e6bd1 100644 --- a/src/function_app.py +++ b/src/function_app.py @@ -39,4 +39,4 @@ def save_snippet(file: func.Out[str], snippetname: str, snippet: str) -> str: file.set(snippet) logging.info(f"Saved snippet: {snippetname}") - return f"Snippet '{ snippetname}' saved successfully with content: {snippet}" + return f"Snippet '{snippetname}' saved successfully with content: {snippet}" From b6d9b7aae80c878c8fc434667d627e283f9c5ae0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:41:22 +0000 Subject: [PATCH 4/5] Fix formatting in README.md code example Co-authored-by: vrdmr <1047040+vrdmr@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf82117..32922e1 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,7 @@ def save_snippet(file: func.Out[str], snippetname: str, snippet: str) -> str: file.set(snippet) logging.info(f"Saved snippet: {snippetname}") - return f"Snippet '{ snippetname}' saved successfully with content: {snippet}" + return f"Snippet '{snippetname}' saved successfully with content: {snippet}" ``` Key features of the new MCP decorators: From f0e0ec1cda9d8aa4ca8debef1da27bfaa4f92c9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:42:11 +0000 Subject: [PATCH 5/5] Fix logging to match original behavior (log snippet content) Co-authored-by: vrdmr <1047040+vrdmr@users.noreply.github.com> --- README.md | 2 +- src/function_app.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32922e1..c60e1f0 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ def save_snippet(file: func.Out[str], snippetname: str, snippet: str) -> str: return "No snippet content provided" file.set(snippet) - logging.info(f"Saved snippet: {snippetname}") + logging.info(f"Saved snippet: {snippet}") return f"Snippet '{snippetname}' saved successfully with content: {snippet}" ``` diff --git a/src/function_app.py b/src/function_app.py index e2e6bd1..9d468cf 100644 --- a/src/function_app.py +++ b/src/function_app.py @@ -38,5 +38,5 @@ def save_snippet(file: func.Out[str], snippetname: str, snippet: str) -> str: return "No snippet content provided" file.set(snippet) - logging.info(f"Saved snippet: {snippetname}") + logging.info(f"Saved snippet: {snippet}") return f"Snippet '{snippetname}' saved successfully with content: {snippet}"