Skip to content

Migrate function_app.py from generic_trigger/generic_*_binding to new @app.mcp_tool() decorators (azure-functions 1.25.0b2) #24

@vrdmr

Description

@vrdmr

Summary

The sample currently uses @app.generic_trigger(type="mcpToolTrigger", ...) and @app.generic_input_binding / @app.generic_output_binding decorators with a manual ToolProperty helper class and JSON-serialized toolProperties. As of azure-functions==1.25.0b2, first-class MCP decorators are available that dramatically simplify this code. The sample should be updated to use them.

Motivation

  • The current code requires a boilerplate ToolProperty class, manual JSON serialization of tool properties, and parsing context JSON to extract arguments — all of which the new decorators eliminate.
  • The new decorators infer tool properties (name, type, description) directly from the Python function signature and docstrings, aligning with standard Python patterns.
  • This sample is a quickstart template; it should showcase the latest recommended API surface.

Prerequisites / Environment Changes

src/requirements.txt

Pin the new beta version:

azure-functions==1.25.0b2

src/local.settings.json

Add the required worker isolation setting:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1"
  }
}

Deployed App Settings (infra / bicep / azd)

Ensure PYTHON_ISOLATE_WORKER_DEPENDENCIES: 1 is also set in the deployed Function App configuration (bicep / app settings).


Required Code Changes in src/function_app.py

1. Remove the ToolProperty helper class and all related boilerplate

Delete the entire ToolProperty class definition, the tool_properties_save_snippets_object / tool_properties_get_snippets_object lists, and the tool_properties_save_snippets_json / tool_properties_get_snippets_json JSON serialization lines. These are no longer needed — the new decorator infers tool properties from the function signature.

2. Migrate hello_mcp

Current:

@app.generic_trigger(
    arg_name="context",
    type="mcpToolTrigger",
    toolName="hello_mcp",
    description="Hello world.",
    toolProperties="[]",
)
def hello_mcp(context) -> None:
    return "Hello I am MCPTool!"

New:

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

Key changes:

  • @app.generic_trigger(...)@app.mcp_tool()
  • The tool description is now inferred from the function docstring.
  • The toolName is inferred from the function name.
  • No context arg is needed when the tool has no properties.
  • Return type should be str, not None.

3. Migrate get_snippet

Current:

@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:
    snippet_content = file.read().decode("utf-8")
    logging.info(f"Retrieved snippet: {snippet_content}")
    return snippet_content

New:

@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

Key changes:

  • @app.generic_trigger(...)@app.mcp_tool()
  • @app.generic_input_binding(...)@app.blob_input(...); the type="blob" parameter is no longer needed (it is implicit).
  • toolProperties JSON is replaced by @app.mcp_tool_property(...) input binding and/or function parameter type hints. Args in the function signature that don't map to another binding are automatically treated as MCP tool properties.
  • context parameter is removed; MCP arguments are received as named function parameters (snippetname).

4. Migrate save_snippet

Current:

@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:
        return "No snippet name provided"
    if not snippet_content_from_args:
        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"

New:

@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:
        return "No snippet content provided"

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

Key changes:

  • @app.generic_trigger(...)@app.mcp_tool()
  • @app.generic_output_binding(...)@app.blob_output(...); type="blob" is implicit.
  • Manual json.loads(context) and content["arguments"][...] extraction is eliminated — arguments arrive as named function parameters.
  • @app.mcp_tool_property(...) decorators explicitly set descriptions for each tool property. These are optional if the defaults are acceptable; properties are inferred from the signature.

5. Clean up imports

The json import can likely be removed since context is no longer manually parsed. Keep logging and azure.functions as func.


How new decorator property inference works (for reference)

Scenario Behavior
Arg has a type hint but no mcp_tool_property Type and name inferred from signature; description left empty
Arg has mcp_tool_property Values from the decorator take precedence over inferred values
Both type hint and property_type in decorator Decorator value wins
Arg matches another binding's arg_name (e.g. file in blob_output) Excluded from MCP tool properties automatically
Arg has Optional[T] type hint Treated as an optional tool property

Documentation Updates

The README.md Source Code section includes inline code snippets of the old API. These must be updated to reflect the new decorators. The prerequisites section should also note the minimum azure-functions version.

Checklist

  • Update src/requirements.txt to pin azure-functions==1.25.0b2
  • Add PYTHON_ISOLATE_WORKER_DEPENDENCIES: 1 to src/local.settings.json
  • Add PYTHON_ISOLATE_WORKER_DEPENDENCIES: 1 to deployed app settings (infra/bicep)
  • Remove ToolProperty class and all tool_properties_* boilerplate from function_app.py
  • Migrate hello_mcp to @app.mcp_tool()
  • Migrate get_snippet to @app.mcp_tool() + @app.blob_input() + @app.mcp_tool_property()
  • Migrate save_snippet to @app.mcp_tool() + @app.blob_output() + @app.mcp_tool_property()
  • Remove json import (no longer needed)
  • Update README.md Source Code section with new decorator examples
  • Test locally with Azurite + MCP Inspector
  • Test deployed with azd up

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions