|
| 1 | +# Host remote MCP servers built with official MCP SDKs on Azure Functions (early preview) |
| 2 | + |
| 3 | +This repo contains instructions and sample for running MCP server built with the C# (.NET) MCP SDK on Azure Functions. The repo uses the weather sample server to demonstrate how this can be done. You can clone to run and test the server locally, follow by easy deploy with `azd up` to have it in the cloud in a few minutes. |
| 4 | + |
| 5 | +**Watch the video overview** |
| 6 | + |
| 7 | +<a href="https://www.youtube.com/watch?v=PAxBlQ9mFv8" target="_blank"> |
| 8 | + <img src="./media/video-thumbnail.jpg" alt="Watch the video" width="500" /> |
| 9 | +</a> |
| 10 | + |
| 11 | +## Running MCP server as custom handler on Azure Functions |
| 12 | + |
| 13 | +Recently Azure Functions released the [Functions MCP extension](https://techcommunity.microsoft.com/blog/appsonazureblog/build-ai-agent-tools-using-remote-mcp-with-azure-functions/4401059), allowing developers to build MCP servers using Functions programming model, which is essentially Function's event-driven framework, and host them remotely on the serverless platform. |
| 14 | + |
| 15 | +For those who have already built servers with [Anthropic's MCP SDKs](https://github.com/modelcontextprotocol/servers?tab=readme-ov-file#model-context-protocol-servers), it's also possible to host the servers on Azure Functions by running them as _custom handlers_, which are lightweight web servers that receive events from the Functions host. They allow you to host your already-built MCP servers with no code changes and benefit from Function's bursty scale, serverless pricing model, and security features. |
| 16 | + |
| 17 | +This repo focuses on the second hosting scenario: |
| 18 | + |
| 19 | +<div align="center"> |
| 20 | + <img src="./media/function_hosting.png" alt="Diagram showing hosting of Function app and custom handler apps." width="500"> |
| 21 | +</div> |
| 22 | + |
| 23 | +## Prerequisites |
| 24 | + |
| 25 | +Ensure you have the following: |
| 26 | + |
| 27 | +* [Azure subscription](https://azure.microsoft.com/free/dotnet/) (you can create one for free) |
| 28 | +* [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd) v1.17.2 or above |
| 29 | +* [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-typescript) v4.5.0 or above |
| 30 | +* [Visual Studio Code](https://code.visualstudio.com/) |
| 31 | +* [Azure Functions extension on Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) |
| 32 | +* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) |
| 33 | + |
| 34 | +>[!NOTE] |
| 35 | +>This sample requires that you have permission to create a [Microsoft Entra app](https://docs.azure.cn/entra/fundamentals/what-is-entra) in the Azure subscription you use. |
| 36 | +
|
| 37 | +## If you already have an existing server... |
| 38 | + |
| 39 | +>[!IMPORTANT] |
| 40 | +>Your server must be **stateless** and uses the **streamable-http** transport to be hosted remotely on Azure Functions today. |
| 41 | +
|
| 42 | +The following instructions will pull in artifacts required for local server testing and deployment. The most important are: `host.json`, `local.settings.json`, and `infra`. Azure Functions only requires the first two JSON files. The `infra` directory isn't a requirement, but it's handy for provisioning Azure resources. |
| 43 | + |
| 44 | +It's unlikely that your project would have files and directory with the same names, but if it does, you'll need to rename them so they won't be overwritten. |
| 45 | + |
| 46 | +Once you've done the necessary renaming, follow these steps: |
| 47 | +1. Inside the MCP server project, run `azd init --template self-hosted-mcp-scaffold-dotnet`. |
| 48 | +1. Answer the prompts |
| 49 | + - Continue initializing an app in '/your/mcp/project/folder'?: Select Yes. |
| 50 | + - Files present both locally and in template: Likely the only one is README, and you can keep the existing. |
| 51 | + - Enter a unique environment name: This will become the name of the resource group the server is deployed in. |
| 52 | +1. In `host.json`: |
| 53 | + - Ensure the executable path and arguments match your .NET project's compiled DLL |
| 54 | + - Ensure the `port` value is the same as the one used by the MCP server |
| 55 | +1. Follow instructions starting in the [Test the server locally](#test-the-server-locally) section. |
| 56 | + |
| 57 | +You can find out more details about the template (coming soon). |
| 58 | + |
| 59 | +## If you're starting from scratch... |
| 60 | + |
| 61 | +Clone the repo and open the sample in Visual Studio Code |
| 62 | + |
| 63 | + ```shell |
| 64 | + git clone https://github.com/Azure-Samples/mcp-sdk-functions-hosting-dotnet.git |
| 65 | + ``` |
| 66 | + |
| 67 | +## Test the server locally |
| 68 | +>[!NOTE] |
| 69 | +>Oct 31 2024 update: Running the server locally requires Azure Functions Core Tools v4.5.0, which is not yet released. Skip to [next section](#register-resource-provider-before-deploying) to prepare for deployment. |
| 70 | +
|
| 71 | +1. In the root directory, run `func start` to build the project and start the server locally |
| 72 | +1. Open _mcp.json_ (in the _.vscode_ directory) |
| 73 | +1. Start the server by selecting the _Start_ button above the **local-mcp-server** |
| 74 | +1. Click on the Copilot icon at the top to open chat (or `Ctrl+Command+I / Ctrl+Alt+I`), and then change to _Agent_ mode in the question window. |
| 75 | +1. Click the tools icon and make sure **local-mcp-server** is checked for Copilot to use in the chat: |
| 76 | + |
| 77 | + <img src="./media/mcp-tools.png" width="200" alt="MCP tools list screenshot"> |
| 78 | +1. Once the server displays the number of tools available, ask "What is the weather in NYC?" Copilot should call one of the weather tools to help answer this question. |
| 79 | + |
| 80 | +## Register resource provider before deploying |
| 81 | + |
| 82 | +Before deploying, you need to register the `Microsoft.App` resource provider: |
| 83 | +```shell |
| 84 | +az provider register --namespace 'Microsoft.App' |
| 85 | +``` |
| 86 | + |
| 87 | +Wait a few seconds for registration to complete. You can check status by using: |
| 88 | +```shell |
| 89 | +az provider show -n Microsoft.App |
| 90 | +``` |
| 91 | + |
| 92 | +## Deployment |
| 93 | + |
| 94 | +1. This sample uses Visual Studio Code as the main client. Configure it as an allowed client application: |
| 95 | + ```shell |
| 96 | + azd env set PRE_AUTHORIZED_CLIENT_IDS aebc6443-996d-45c2-90f0-388ff96faa56 |
| 97 | + ``` |
| 98 | + |
| 99 | +1. Specify a service management reference if required by your organization. If you're not a Microsoft employee and don't know that you need to set this, you can skip this step. However, if provisioning fails with an error about a missing service management reference, you may need to revisit this step. Microsoft employees using a Microsoft tenant must provide a service management reference (your Service Tree ID). Without this, you won't be able to create the Entra app registration, and provisioning will fail. |
| 100 | + ```shell |
| 101 | + azd env set SERVICE_MANAGEMENT_REFERENCE <service-management-reference> |
| 102 | + ``` |
| 103 | +
|
| 104 | +1. Run `azd up` in the root directory. Then pick an Azure subcription to deploy resources to and select from the available regions. |
| 105 | +
|
| 106 | + When the deployment finishes, your terminal will display output similar to the following: |
| 107 | +
|
| 108 | + ```shell |
| 109 | + (✓) Done: Resource group: rg-resource-group-name (12.061s) |
| 110 | + (✓) Done: App Service plan: plan-random-guid (6.748s) |
| 111 | + (✓) Done: Virtual Network: vnet-random-guid (8.566s) |
| 112 | + (✓) Done: Log Analytics workspace: log-random-guid (29.422s) |
| 113 | + (✓) Done: Storage account: strandomguid (34.527s) |
| 114 | + (✓) Done: Application Insights: appi-random-guid (8.625s) |
| 115 | + (✓) Done: Function App: func-mcp-random-guid (36.096s) |
| 116 | + (✓) Done: Private Endpoint: blob-private-endpoint (30.67s) |
| 117 | +
|
| 118 | + Deploying services (azd deploy) |
| 119 | + (✓) Done: Deploying service api |
| 120 | + - Endpoint: https://functionapp-name.azurewebsites.net/ |
| 121 | + ``` |
| 122 | +
|
| 123 | +### Connect to server on Visual Studio Code |
| 124 | +
|
| 125 | +1. Open _mcp.json_ in the editor. |
| 126 | +1. Stop the local server by selecting the _Stop_ button above the **local-mcp-server**. |
| 127 | +1. Start the remote server by selecting the _Start_ button above the **remote-mcp-server**. |
| 128 | +1. Visual Studio Code will prompt you for the Function App domain. Copy it from either the terminal output or the Portal. |
| 129 | +1. Open Copilot in Agent mode and make sure **remote-mcp-server** is checked in the tool's list. |
| 130 | +1. VS Code should prompt you to authenticate to Microsoft. Click _Allow_, and then login into your Microsoft account (the one used to access Azure Portal). |
| 131 | +1. Ask Copilot "What is the weather in Seattle?". It should call one of the weather tools to help answer. |
| 132 | + |
| 133 | +>[!TIP] |
| 134 | +>In addition to starting an MCP server in _mcp.json_, you can see output of a server by clicking _More..._ -> _Show Output_. The output provides useful information like why a connection might've failed. |
| 135 | +> |
| 136 | +>You can also click the gear icon to change log levels to "Traces" to get even more details on the interactions between the client (Visual Studio Code) and the server. |
| 137 | +> |
| 138 | +><img src="./media/log-level.png" width="200" alt="Log level screenshot"> |
| 139 | +
|
| 140 | +### Redeployment |
| 141 | +
|
| 142 | +If you want to redeploy the server after making changes, there are different options: |
| 143 | +
|
| 144 | +1. Run `azd deploy`. (See azd command [reference](https://learn.microsoft.com/azure/developer/azure-developer-cli/reference).) |
| 145 | +1. Open command palette in Visual Studio Code (`Command+Shift+P/Cntrl+Shift+P`) and search for **Azure Functions: Deploy to Function App**. Then select the name of the function app to deploy to. |
| 146 | +
|
| 147 | +## Built-in server authorization with Easy Auth |
| 148 | +
|
| 149 | +The server app is configured with [Easy Auth](https://learn.microsoft.com/azure/app-service/overview-authentication-authorization), which implements the requirements of the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-discovery), such as issuing 401 challenge and exposing a Protected Resource Metadata (PRM). |
| 150 | +
|
| 151 | +When the server is configured to use Easy Auth for MCP Server authorization, you should see the following sequence of events in the debug output from Visual Studio Code: |
| 152 | +
|
| 153 | +1. The editor sends an initialization request to the MCP server. |
| 154 | +1. The MCP server responds with an error indicating that authorization is required. The response includes a pointer to the protected resource metadata (PRM) for the application. The Easy Auth feature generates the PRM for an app built with this sample. |
| 155 | +1. The editor fetches the PRM and uses it to identify the authorization server. |
| 156 | +1. The editor attempts to obtain authorization server metadata (ASM) from a well-known endpoint on the authorization server. |
| 157 | +1. Microsoft Entra ID doesn't support ASM on the well-known endpoint, so the editor falls back to using the OpenID Connect metadata endpoint to obtain the ASM. It tries to discover this using by inserting the well-known endpoint before any other path information. |
| 158 | +1. The OpenID Connect specifications actually defined the well-known endpoint as being after path information, and that is where Microsoft Entra ID hosts it. So the editor tries again with that format. |
| 159 | +1. The editor successfully retrieves the ASM. It then can then use this information in conjunction with its own client ID to perform a login. At this point, the editor prompts you to sign in and consent to the application. |
| 160 | +1. Assuming you successfully sign in and consent, the editor completes the login. It repeats the intialization request to the MCP server, this time including an authorization token in the request. This reattempt isn't visible at the Debug output level, but you can see it in the Trace output level. |
| 161 | +1. The MCP server validates the token and responds with a successful response to the initialization request. The standard MCP flow continues from this point, ultimately resulting in discovery of the MCP tool defined in this sample. |
| 162 | +
|
| 163 | +### Support for other clients |
| 164 | +
|
| 165 | +Other than Visual Studio Code, agents in Azure AI Foundry can also connect to Function-hosted MCP servers that are configured with Easy Auth. Docs coming soon. |
| 166 | +
|
| 167 | +## Clean up resources |
| 168 | +
|
| 169 | +When you're done working with your server, you can use this command to delete the resources created on Azure and avoid incurring any further costs: |
| 170 | + |
| 171 | + ```shell |
| 172 | + azd down |
| 173 | + ``` |
| 174 | + |
| 175 | +## Next steps |
| 176 | + |
| 177 | +### Find this sample in other languages |
| 178 | + |
| 179 | +| Language (Stack) | Repo Location | |
| 180 | +|------------------|---------------| |
| 181 | +| Python | [mcp-sdk-functions-hosting-python](https://github.com/Azure-Samples/mcp-sdk-functions-hosting-python) | |
| 182 | +| Node | [mcp-sdk-functions-hosting-node](https://github.com/Azure-Samples/mcp-sdk-functions-hosting-node) | |
| 183 | + |
| 184 | +## Troubleshooting |
| 185 | +The following are some common issues that come up. |
| 186 | + |
| 187 | +1. **InternalServerError: There was an unexpected InternalServerError. Please try again later.** |
| 188 | + |
| 189 | + Check if you have registered the `Microsoft.App` resource provider: |
| 190 | + |
| 191 | + ```shell |
| 192 | + az provider show -n Microsoft.App |
| 193 | + ``` |
| 194 | + |
| 195 | + If it's showing up as unregistered, register it: |
| 196 | + ```shell |
| 197 | + az provider register --namespace 'Microsoft.App' |
| 198 | + ``` |
| 199 | +
|
| 200 | + Successful registration should show: |
| 201 | + ```shell |
| 202 | + Namespace RegistrationPolicy RegistrationState |
| 203 | + ------------- -------------------- ------------------- |
| 204 | + Microsoft.App RegistrationRequired Registered |
| 205 | + ``` |
| 206 | +
|
| 207 | + Then run `azd up` again. |
| 208 | +
|
| 209 | +2. **Error: error executing step command 'deploy --all': getting target resource: resource not found: unable to find a resource tagged with 'azd-server-name: api'. Ensure the service resource is corrected tagged in your infrastructure configuration, and rerun provision** |
| 210 | +
|
| 211 | + This is a [known transient error](https://github.com/Azure/azure-dev/issues/5580). Try re-running `azd up`. |
| 212 | +
|
| 213 | +3. **Need admin approval. Visual Studio Code needs permission to access resources in your organization that only an admin can grant. Please ask an admin to grant permission to this app before you can use it.** |
| 214 | +
|
| 215 | + This means your Entra app hasn't authorized VS Code as a trusted client. To fix this issue, go to the Azure Portal and search for your Entra app ("MCP Authorization App") in the global search bar. Inside the Entra app resource, click on **Expose an API** in the left menu. Look for the **+ Add a client application** button. Click to add VS Code's Client ID `aebc6443-996d-45c2-90f0-388ff96faa56`, remembering to check the authorized scopes box and click **Add application**: |
| 216 | +
|
| 217 | + <img src="./media/add-vscode-id.png" width="400" alt="Add VS Code client ID screenshot"> |
| 218 | +
|
| 219 | +4. **Connection state: Error Error sending message to {endpoint}: TypeError: fetch failed** |
| 220 | + |
| 221 | + Ensure the Function app domain is correct when connecting to the server. |
| 222 | +
|
| 223 | +5. **Ensure you have the latest version of Azure Functions Core Tools installed.** |
| 224 | + |
| 225 | + You need [version >=4.4.0](https://learn.microsoft.com/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-typescript). Check by running `func --version`. |
| 226 | +
|
| 227 | +6. **`.vscode/mcp.json` must be in the root for VS Code to detect MCP server registration** |
| 228 | +
|
| 229 | + If you don't see the _Start_ button above server registrations, it's likely because `.vscode/mcp.json` isn't located in the root of your workspace folder. |
| 230 | + |
0 commit comments