Skip to content

nzthiago/remote-mcp-functions-python

 
 

Repository files navigation

Getting Started with Remote MCP Servers using Azure Functions (Python)

This is a quickstart template to easily build and deploy a custom remote MCP server to the cloud using Azure Functions with Python. You can clone/restore/run on your local machine with debugging, and azd up to have it in the cloud in a couple minutes. The MCP server is secured by design using keys and HTTPS, and allows more options for OAuth using EasyAuth and/or API Management as well as network isolation using VNET.

If you're looking for this sample in more languages check out the .NET/C# and Node.js/TypeScript versions.

Open in GitHub Codespaces

Below is the architecture diagram for the Remote MCP Server using Azure Functions:

Architecture Diagram

Prerequisites

Prepare your local environment

An Azure Storage Emulator is needed for this particular sample because we will save and get snippets from blob storage.

  1. Start Azurite

    docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 \
        mcr.microsoft.com/azure-storage/azurite

Note if you use Azurite coming from VS Code extension you need to run Azurite: Start now or you will see errors.

Run your MCP Server locally from the terminal

  1. Change to the src folder in a new terminal window

    cd src
  2. Install required extensions

    func extensions install

Note if you miss this step the function will not start

  1. Install Python dependencies

    pip install -r requirements.txt
  2. Start the Functions host locally:

    func start

Note by default this will use the webhooks route: /runtime/webhooks/mcp/sse. Later we will use this in Azure to set the key on client/host calls: /runtime/webhooks/mcp/sse?code=<system_key>

Use the MCP server from within a client/host

VS Code - Copilot Edits

  1. Add MCP Server from command palette and add URL to your running Function app's SSE endpoint:

    http://0.0.0.0:7071/runtime/webhooks/mcp/sse
  2. List MCP Servers from command palette and start the server

  3. In Copilot chat agent mode enter a prompt to trigger the tool, e.g., select some code and enter this prompt

    Say Hello
    
    Save this snippet as snippet1 
    
    Retrieve snippet1 and apply to newFile.py
    
  4. When prompted to run the tool, consent by clicking Continue

  5. When you're done, press Ctrl+C in the terminal window to stop the Functions host process.

MCP Inspector

  1. In a new terminal window, install and run MCP Inspector

    npx @modelcontextprotocol/inspector
  2. CTRL click to load the MCP Inspector web app from the URL displayed by the app (e.g. http://0.0.0.0:5173/#resources)

  3. Set the transport type to SSE

  4. Set the URL to your running Function app's SSE endpoint and Connect:

    http://0.0.0.0:7071/runtime/webhooks/mcp/sse
  5. List Tools. Click on a tool and Run Tool.

Deploy to Azure for Remote MCP

Run this azd command to provision the function app, with any required Azure resources, and deploy your code:

azd up

Using key based auth This function requires a system key by default which can be obtained from the portal, and then update the URL in your host/client to be: https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp/sse?code=<systemkey_for_mcp_extension>

Via command line you can retrieve the function key with:

# After azd up has completed at least once
FUNCTION_APP_NAME=$(cat .azure/$(cat .azure/config.json | jq -r '.defaultEnvironment')/env.json | jq -r '.FUNCTION_APP_NAME')
RESOURCE_GROUP=$(cat .azure/$(cat .azure/config.json | jq -r '.defaultEnvironment')/env.json | jq -r '.AZURE_RESOURCE_GROUP')
az functionapp keys list --resource-group $RESOURCE_GROUP --name $FUNCTION_APP_NAME

Additionally, API Management can be used for improved security and policies over your MCP Server, and EasyAuth can be used to set up your favorite OAuth provider including Entra.

You can opt-in to a VNet being used in the sample. To do so, do this before azd up:

azd env set VNET_ENABLED true

After publish completes successfully, azd provides you with the URL endpoints of your new functions, but without the function key values required to access the endpoints. To obtain these same endpoints along with the required function keys, use the command shown above or see Invoke the function on Azure.

Redeploy your code

You can run the azd up command as many times as you need to both provision your Azure resources and deploy code updates to your function app.

Note

Deployed code files are always overwritten by the latest deployment package.

Clean up resources

When you're done working with your function app and related resources, you can use this command to delete the function app and its related resources from Azure and avoid incurring any further costs:

azd down

Helpful Azure Commands

Once your application is deployed, you can use these commands to manage and monitor your application:

# Get your function app name from the environment file
FUNCTION_APP_NAME=$(cat .azure/$(cat .azure/config.json | jq -r '.defaultEnvironment')/env.json | jq -r '.FUNCTION_APP_NAME')
echo $FUNCTION_APP_NAME

# Get resource group 
RESOURCE_GROUP=$(cat .azure/$(cat .azure/config.json | jq -r '.defaultEnvironment')/env.json | jq -r '.AZURE_RESOURCE_GROUP')
echo $RESOURCE_GROUP

# View function app logs
az webapp log tail --name $FUNCTION_APP_NAME --resource-group $RESOURCE_GROUP

# Redeploy the application without provisioning new resources
azd deploy

Source Code

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:

@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.
    """
    return "Hello I am MCPTool!"


@app.generic_trigger(
    arg_name="context",
    type="mcpToolTrigger",
    toolName="getsnippet",
    description="Retrieve a snippet by name.",
    toolProperties=f"[{{\"propertyName\":\"{_SNIPPET_NAME_PROPERTY_NAME}\",\"propertyType\":\"string\",\"description\":\"The name of the snippet.\"}}]"
)
@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.
    """
    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=f"[{{\"propertyName\":\"{_SNIPPET_NAME_PROPERTY_NAME}\",\"propertyType\":\"string\",\"description\":\"The name of the snippet.\"}},"
                   f"{{\"propertyName\":\"{_SNIPPET_PROPERTY_NAME}\",\"propertyType\":\"string\",\"description\":\"The content of the snippet.\"}}]"
)
@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"

Next Steps

About

No description, website, or topics provided.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Bicep 85.2%
  • Python 14.8%