diff --git a/azurefunctions-extensions-mcp/LICENSE b/azurefunctions-extensions-mcp/LICENSE new file mode 100644 index 0000000..63447fd --- /dev/null +++ b/azurefunctions-extensions-mcp/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/MANIFEST.txt b/azurefunctions-extensions-mcp/MANIFEST.txt new file mode 100644 index 0000000..e1ae5ad --- /dev/null +++ b/azurefunctions-extensions-mcp/MANIFEST.txt @@ -0,0 +1,3 @@ +recursive-include azure *.py *.pyi +recursive-include tests *.py +include LICENSE README.md \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/README.md b/azurefunctions-extensions-mcp/README.md new file mode 100644 index 0000000..0326d87 --- /dev/null +++ b/azurefunctions-extensions-mcp/README.md @@ -0,0 +1,102 @@ +# Azure Functions Extensions Bindings ServiceBus library for Python +This library allows ServiceBus Triggers in Python Function Apps to recognize and bind to client types from the +Azure ServiceBus sdk. + +The SDK types can be generated from: + +* ServiceBus Triggers + +The supported ServiceBus SDK types include: + +* ServiceBusReceivedMessage + +[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-servicebus) +| +[Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-servicebus/) +| [Samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-servicebus/samples) + + +## Getting started + +### Prerequisites +* Python 3.9 or later is required to use this package. For more details, please read our page on [Python Functions version support policy](https://learn.microsoft.com/en-us/azure/azure-functions/functions-versions?tabs=isolated-process%2Cv4&pivots=programming-language-python#languages). + +* You must have an [Azure subscription](https://azure.microsoft.com/free/) and a +[ServiceBus Resource](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus?tabs=isolated-process%2Cextensionv5%2Cextensionv3&pivots=programming-language-python) to use this package. + +### Install the package +Install the Azure Functions Extensions Bindings ServiceBus library for Python with pip: + +```bash +pip install azurefunctions-extensions-bindings-servicebus +``` + + +### Bind to the SDK-type +The Azure Functions Extensions Bindings ServiceBus library for Python allows you to create a function app with a ServiceBus Trigger +and define the type as a ServiceBusReceivedMessage. Instead of receiving +a ServiceBusMessage, when the function is executed, the type returned will be the defined SDK-type and have all the +properties and methods available as seen in the Azure ServiceBus library for Python. + + +```python +import logging +import azure.functions as func +import azurefunctions.extensions.bindings.servicebus as servicebus + +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) + +@app.service_bus_queue_trigger(arg_name="receivedmessage", + queue_name="QUEUE_NAME", + connection="SERVICEBUS_CONNECTION") +def servicebus_queue_trigger(receivedmessage: servicebus.ServiceBusReceivedMessage): + logging.info("Python ServiceBus queue trigger processed message.") + logging.info("Receiving: %s\n" + "Body: %s\n" + "Enqueued time: %s\n" + "Lock Token: %s\n" + "Locked until : %s\n" + "Message ID: %s\n" + "Sequence number: %s\n", + receivedmessage, + receivedmessage.body, + receivedmessage.enqueued_time_utc, + receivedmessage.lock_token, + receivedmessage.locked_until, + receivedmessage.message_id, + receivedmessage.sequence_number) +``` + +## Troubleshooting +### General +The SDK-types raise exceptions defined in [Azure Core](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/README.md). + +This list can be used for reference to catch thrown exceptions. To get the specific error code of the exception, use the `error_code` attribute, i.e, `exception.error_code`. + +## Next steps + +### More sample code + +Get started with our [ServiceBus samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-servicebus/samples). + +Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Azure ServiceBus: + +* [servicebus_samples_single](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-servicebus/samples/servicebus_samples_single) - Examples for using the ServiceBusReceivedMessage type: + * From ServiceBus Queue Trigger (Single Message) + * From ServiceBus Topic Trigger (Single Message) + +* [servicebus_samples_batch](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-servicebus/samples/service_samples_batch) - Examples for interacting with batches: + * From ServiceBus Queue Trigger (Batch) + * From ServiceBus Topic Trigger (Batch) + + +### Additional documentation +For more information on the Azure ServiceBus SDK, see the [Azure ServiceBus SDK documentation](https://learn.microsoft.com/en-us/python/api/overview/azure/servicebus-readme?view=azure-python) on docs.microsoft.com +and the [Azure ServiceBus README](https://github.com/Azure/azure-sdk-for-python/blob/azure-servicebus_7.14.1/sdk/servicebus/azure-servicebus/README.md). + +## Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/azurefunctions/__init__.py b/azurefunctions-extensions-mcp/azurefunctions/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azurefunctions-extensions-mcp/azurefunctions/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azurefunctions-extensions-mcp/azurefunctions/extensions/__init__.py b/azurefunctions-extensions-mcp/azurefunctions/extensions/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azurefunctions-extensions-mcp/azurefunctions/extensions/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/__init__.py b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/__init__.py new file mode 100644 index 0000000..fec3a63 --- /dev/null +++ b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from .decorators.mcp_app import McpApp, MCPToolContext + +__all__ = [ + "McpApp", + "MCPToolContext", +] + +__version__ = '0.0.0a1' diff --git a/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/constants.py b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/constants.py new file mode 100644 index 0000000..53bad1f --- /dev/null +++ b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/constants.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +MCP_TOOL_TRIGGER = "mcpToolTrigger" \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/__init__.py b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/__init__.py new file mode 100644 index 0000000..fd94462 --- /dev/null +++ b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Decorator definitions for MCP Function Apps.""" \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/mcp.py b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/mcp.py new file mode 100644 index 0000000..1b7b4be --- /dev/null +++ b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/mcp.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from ..constants import ( + MCP_TOOL_TRIGGER +) + +from typing import Any, Dict, Optional +from azure.functions.decorators.core import Trigger, DataType + + +class MCPToolTrigger(Trigger): + + @staticmethod + def get_binding_name() -> str: + return MCP_TOOL_TRIGGER + + def __init__(self, + name: str, + tool_name: str, + description: Optional[str] = None, + tool_properties: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.tool_name = tool_name + self.description = description + self.tool_properties = tool_properties + super().__init__(name=name, data_type=data_type) + + +# MCP-specific context object +class MCPToolContext(Dict[str, Any]): + pass diff --git a/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/mcp_app.py b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/mcp_app.py new file mode 100644 index 0000000..798cc51 --- /dev/null +++ b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/decorators/mcp_app.py @@ -0,0 +1,103 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from ..utils import _extract_type_and_description +from .mcp import MCPToolTrigger, MCPToolContext + +import inspect +import json +from typing import Callable + +from azure.functions import FunctionRegister, Blueprint, AuthLevel + + +# Map Python types to MCP property types +_TYPE_MAPPING = { + int: "integer", + float: "number", + str: "string", + bool: "boolean", +} + +class McpApp(Blueprint, FunctionRegister): + """ + MCP-specific app object. + Extends FunctionRegister to reuse function registration and trigger infrastructure. + """ + + def __init__(self, auth_level=AuthLevel.ANONYMOUS): + super().__init__(auth_level=auth_level) + + def mcp_tool(self) -> Callable[[Callable], Callable]: + """ + Decorator to register an MCP tool function. + + Automatically: + - Infers tool name from function name + - Extracts first line of docstring as description + - Extracts parameters and types for tool properties + - Handles MCPToolContext injection + """ + + def decorator(target_func: Callable) -> Callable: + # Extract function metadata + sig = inspect.signature(target_func) + tool_name = target_func.__name__ + description = (target_func.__doc__ or "").strip().split("\n")[0] + + # Build tool properties metadata + tool_properties = [] + for param_name, param in sig.parameters.items(): + param_type_hint = param.annotation if param.annotation != inspect.Parameter.empty else str + actual_type, param_description = _extract_type_and_description(param_name, param_type_hint) + if actual_type is MCPToolContext: + continue # context is injected, not a property + property_type = _TYPE_MAPPING.get(actual_type, "string") + tool_properties.append({ + "propertyName": param_name, + "propertyType": property_type, + "description": param_description, + }) + + tool_properties_json = json.dumps(tool_properties) + + # Wrapper function for MCP trigger + def wrapper(context: str) -> str: + try: + content = json.loads(context) + arguments = content.get("arguments", {}) + kwargs = {} + + for param_name, param in sig.parameters.items(): + param_type_hint = param.annotation if param.annotation != inspect.Parameter.empty else str + actual_type, _ = _extract_type_and_description(param_name, param_type_hint) + + if actual_type is MCPToolContext: + kwargs[param_name] = content + elif param_name in arguments: + kwargs[param_name] = arguments[param_name] + else: + return f"Error: Missing required parameter '{param_name}' for '{tool_name}'" + + result = target_func(**kwargs) + return str(result) + + except Exception as e: + return f"Error executing function '{tool_name}': {str(e)}" + + wrapper.__name__ = target_func.__name__ + wrapper.__doc__ = target_func.__doc__ + + # Register as MCP trigger using existing azure-functions infrastructure + fb = self._configure_function_builder(lambda fb: fb)(wrapper) + fb.add_trigger( + trigger=MCPToolTrigger( + name="context", + tool_name=tool_name, + description=description, + tool_properties=tool_properties_json + ) + ) + + return fb + + return decorator diff --git a/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/utils.py b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/utils.py new file mode 100644 index 0000000..d5bfc11 --- /dev/null +++ b/azurefunctions-extensions-mcp/azurefunctions/extensions/mcp/utils.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Any, Tuple, get_args, get_origin, Annotated + + +# Helper to extract actual type and description from Annotated types +def _extract_type_and_description(param_name: str, type_hint: Any) -> Tuple[Any, str]: + if get_origin(type_hint) is Annotated: + args = get_args(type_hint) + actual_type = args[0] + # Use first string annotation as description if present + param_description = next((a for a in args[1:] if isinstance(a, str)), f"The {param_name} parameter.") + return actual_type, param_description + return type_hint, f"The {param_name} parameter." \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/pyproject.toml b/azurefunctions-extensions-mcp/pyproject.toml new file mode 100644 index 0000000..fe807d9 --- /dev/null +++ b/azurefunctions-extensions-mcp/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "func-mcp-test" +dynamic = ["version"] +requires-python = ">=3.10" +authors = [{ name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com"}] +description = "MCP Python worker extension for Azure Functions." +readme = "README.md" +license = {text = "MIT License"} +classifiers= [ + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Environment :: Web Environment', + 'Development Status :: 5 - Production/Stable', + ] +dependencies = [ + 'azure-functions>=1.24.0' + ] + +[project.optional-dependencies] +dev = [ + 'flake8', + 'mypy', + 'pytest', + 'pytest-cov', + 'coverage', + 'pytest-instafail', + 'pre-commit' + ] + +[tool.setuptools.dynamic] +version = {attr = "azurefunctions.extensions.mcp.__version__"} + +[tool.setuptools.packages.find] +exclude = [ + 'azurefunctions.extensions', + 'azurefunctions', 'tests', 'samples' + ] diff --git a/azurefunctions-extensions-mcp/samples/.funcignore b/azurefunctions-extensions-mcp/samples/.funcignore new file mode 100644 index 0000000..b694934 --- /dev/null +++ b/azurefunctions-extensions-mcp/samples/.funcignore @@ -0,0 +1 @@ +.venv \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/samples/.gitignore b/azurefunctions-extensions-mcp/samples/.gitignore new file mode 100644 index 0000000..c6be22e --- /dev/null +++ b/azurefunctions-extensions-mcp/samples/.gitignore @@ -0,0 +1,47 @@ +bin +obj +csx +.vs +edge +Publish + +*.user +*.suo +*.cscfg +*.Cache +project.lock.json + +/packages +/TestResults + +/tools/NuGet.exe +/App_Data +/secrets +/data +.secrets +appsettings.json + +node_modules +dist + +# Local python packages +.python_packages/ + +# Python Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Azurite artifacts +__blobstorage__ +__queuestorage__ +__azurite_db*__.json \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/samples/README.md b/azurefunctions-extensions-mcp/samples/README.md new file mode 100644 index 0000000..7fa8d44 --- /dev/null +++ b/azurefunctions-extensions-mcp/samples/README.md @@ -0,0 +1,278 @@ + + +# 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 built-in auth and/or [API Management](https://aka.ms/mcp-remote-apim-auth) as well as network isolation using VNET. + +If you're looking for this sample in more languages check out the [.NET/C#](https://github.com/Azure-Samples/remote-mcp-functions-dotnet) and [Node.js/TypeScript](https://github.com/Azure-Samples/remote-mcp-functions-typescript) versions. + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/Azure-Samples/remote-mcp-functions-python) + +Below is the architecture diagram for the Remote MCP Server using Azure Functions: + +![Architecture Diagram](architecture-diagram.png) + +## Prerequisites + ++ [Python](https://www.python.org/downloads/) version 3.11 or higher ++ [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local?pivots=programming-language-python#install-the-azure-functions-core-tools) >= `4.0.7030` ++ [Azure Developer CLI](https://aka.ms/azd) ++ To use Visual Studio Code to run and debug locally: + + [Visual Studio Code](https://code.visualstudio.com/) + + [Azure Functions extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) + +## 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 + + ```shell + 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: + + ```shell + cd src + ``` + +1. Install Python dependencies: + + ```shell + pip install -r requirements.txt + ``` + +>**Note** it is a best practice to create a Virtual Environment before doing the `pip install` to avoid dependency issues/collisions, or if you are running in CodeSpaces. See [Python Environments in VS Code](https://code.visualstudio.com/docs/python/environments#_creating-environments) for more information. + +1. Start the Functions host locally: + + ```shell + 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=` + +## Connect to the *local* MCP server from a client/host + +### VS Code - Copilot agent mode + +1. **Add MCP Server** from command palette and add URL to your running Function app's SSE endpoint: + + ```shell + http://0.0.0.0:7071/runtime/webhooks/mcp/sse + ``` + +1. **List MCP Servers** from command palette and start the server +1. In Copilot chat agent mode enter a prompt to trigger the tool, e.g., select some code and enter this prompt + + ```plaintext + Say Hello + ``` + + ```plaintext + Save this snippet as snippet1 + ``` + + ```plaintext + Retrieve snippet1 and apply to newFile.py + ``` + +1. When prompted to run the tool, consent by clicking **Continue** + +1. 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 + + ```shell + 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**: + + ```shell + http://0.0.0.0:7071/runtime/webhooks/mcp/sse + ``` + +>**Note** this step will not work in CodeSpaces. Please move on to Deploy to Remote MCP. + +5. **List Tools**. Click on a tool and **Run Tool**. + +## Verify local blob storage in Azurite + +After testing the snippet save functionality locally, you can verify that blobs are being stored correctly in your local Azurite storage emulator. + +### Using Azure Storage Explorer + +1. Open Azure Storage Explorer +1. In the left panel, expand **Emulator & Attached** → **Storage Accounts** → **(Emulator - Default Ports) (Key)** +1. Navigate to **Blob Containers** → **snippets** +1. You should see any saved snippets as blob files in this container +1. Double-click on any blob to view its contents and verify the snippet data was saved correctly + +### Using Azure CLI (Alternative) + +If you prefer using the command line, you can also verify blobs using Azure CLI with the storage emulator: + +```shell +# List blobs in the snippets container +az storage blob list --container-name snippets --connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" +``` + +```shell +# Download a specific blob to view its contents +az storage blob download --container-name snippets --name --file --connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" +``` + +This verification step ensures your MCP server is correctly interacting with the local storage emulator and that the blob storage functionality is working as expected before deploying to Azure. + +## Deploy to Azure for Remote MCP + +Run this [azd](https://aka.ms/azd) command to provision the function app, with any required Azure resources, and deploy your code: + +```shell +azd up +``` + +You can opt-in to a VNet being used in the sample. To do so, do this before `azd up` + +```bash +azd env set VNET_ENABLED true +``` + +Additionally, [API Management]() can be used for improved security and policies over your MCP Server, and [App Service built-in authentication](https://learn.microsoft.com/azure/app-service/overview-authentication-authorization) can be used to set up your favorite OAuth provider including Entra. + +## Connect to your *remote* MCP server function app from a client + +Your client will need a key in order to invoke the new hosted SSE endpoint, which will be of the form `https://.azurewebsites.net/runtime/webhooks/mcp/sse`. The hosted function requires a system key by default which can be obtained from the [portal](https://learn.microsoft.com/azure/azure-functions/function-keys-how-to?tabs=azure-portal) or the CLI (`az functionapp keys list --resource-group --name `). Obtain the system key named `mcp_extension`. + +### Connect to remote MCP server in MCP Inspector +For MCP Inspector, you can include the key in the URL: +```plaintext +https://.azurewebsites.net/runtime/webhooks/mcp/sse?code= +``` + +### Connect to remote MCP server in VS Code - GitHub Copilot +For GitHub Copilot within VS Code, you should instead set the key as the `x-functions-key` header in `mcp.json`, and you would just use `https://.azurewebsites.net/runtime/webhooks/mcp/sse` for the URL. The following example uses an input and will prompt you to provide the key when you start the server from VS Code. Note [mcp.json](.vscode/mcp.json) has already been included in this repo and will be picked up by VS Code. Click Start on the server to be prompted for values including `functionapp-name` (in your /.azure/*/.env file) and `functions-mcp-extension-system-key` which can be obtained from CLI command above or API Keys in the portal for the Function App. + +```json +{ + "inputs": [ + { + "type": "promptString", + "id": "functions-mcp-extension-system-key", + "description": "Azure Functions MCP Extension System Key", + "password": true + }, + { + "type": "promptString", + "id": "functionapp-name", + "description": "Azure Functions App Name" + } + ], + "servers": { + "remote-mcp-function": { + "type": "sse", + "url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp/sse", + "headers": { + "x-functions-key": "${input:functions-mcp-extension-system-key}" + } + }, + "local-mcp-function": { + "type": "sse", + "url": "http://0.0.0.0:7071/runtime/webhooks/mcp/sse" + } + } +} +``` + +For MCP Inspector, you can include the key in the URL: `https://.azurewebsites.net/runtime/webhooks/mcp/sse?code=`. + +For GitHub Copilot within VS Code, you should instead set the key as the `x-functions-key` header in `mcp.json`, and you would just use `https://.azurewebsites.net/runtime/webhooks/mcp/sse` for the URL. The following example uses an input and will prompt you to provide the key when you start the server from VS Code: + +```json +{ + "inputs": [ + { + "type": "promptString", + "id": "functions-mcp-extension-system-key", + "description": "Azure Functions MCP Extension System Key", + "password": true + } + ], + "servers": { + "my-mcp-server": { + "type": "sse", + "url": ".azurewebsites.net/runtime/webhooks/mcp/sse", + "headers": { + "x-functions-key": "${input:functions-mcp-extension-system-key}" + } + } + } +} +``` + +## 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: + +```shell +azd down +``` + +## Helpful Azure Commands + +Once your application is deployed, you can use these commands to manage and monitor your application: + +```bash +# 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 +``` + +## Next Steps + +- Add [API Management](https://aka.ms/mcp-remote-apim-auth) to your MCP server (auth, gateway, policies, more!) +- Add [built-in auth](https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization) to your MCP server +- Enable VNET using VNET_ENABLED=true flag +- Learn more about [related MCP efforts from Microsoft](https://github.com/microsoft/mcp/tree/main/Resources) diff --git a/azurefunctions-extensions-mcp/samples/function_app.py b/azurefunctions-extensions-mcp/samples/function_app.py new file mode 100644 index 0000000..9b32b9c --- /dev/null +++ b/azurefunctions-extensions-mcp/samples/function_app.py @@ -0,0 +1,17 @@ +from azurefunctions.extensions.mcp import McpApp, MCPToolContext +from typing import Annotated + +app = McpApp() + +@app.mcp_tool() +def add_numbers( + a: Annotated[int, "First number"], + b: Annotated[int, "Second number"] +) -> str: + """Add two integers.""" + return str(a + b) + +@app.mcp_tool() +def greet_user(name: Annotated[str, "User's name"], context: MCPToolContext) -> str: + """Greet the user.""" + return f"Hello, {name}! You called with context: {context}" diff --git a/azurefunctions-extensions-mcp/samples/host.json b/azurefunctions-extensions-mcp/samples/host.json new file mode 100644 index 0000000..3f3fe60 --- /dev/null +++ b/azurefunctions-extensions-mcp/samples/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle.Experimental", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/samples/local.settings.json b/azurefunctions-extensions-mcp/samples/local.settings.json new file mode 100644 index 0000000..504bb06 --- /dev/null +++ b/azurefunctions-extensions-mcp/samples/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python", + "AzureWebJobsStorage": "UseDevelopmentStorage=true" + } + } \ No newline at end of file diff --git a/azurefunctions-extensions-mcp/samples/requirements.txt b/azurefunctions-extensions-mcp/samples/requirements.txt new file mode 100644 index 0000000..2b298a9 --- /dev/null +++ b/azurefunctions-extensions-mcp/samples/requirements.txt @@ -0,0 +1,6 @@ +# Do not include azure-functions-worker in this file +# The Python Worker is managed by the Azure Functions platform +# Manually managing azure-functions-worker may cause unexpected issues + +azure-functions +func-mcp-test