{"id":56474,"date":"2025-04-16T13:30:00","date_gmt":"2025-04-16T20:30:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=56474"},"modified":"2025-04-16T13:26:57","modified_gmt":"2025-04-16T20:26:57","slug":"build-mcp-remote-servers-with-azure-functions","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/build-mcp-remote-servers-with-azure-functions\/","title":{"rendered":"Build MCP Remote Servers with Azure Functions"},"content":{"rendered":"<p>You can&#8217;t run to the grocery store these days without hearing about the Model Context Protocol (MCP)! Well, I hope the grocery store is your safe haven from AI, but the fact is that MCP is one of the hottest and most talked about topics in software development. And I&#8217;m going to keep talking about it because I want to show you a brand new experimental preview feature of Azure Functions that takes a ton of work out of creating remote MCP Servers and brings all the goodness of Azure Functions to the equation too.<\/p>\n<p><div  class=\"d-flex justify-content-left\"><a class=\"cta_button_link btn-primary mb-24\" href=\"https:\/\/aka.ms\/cadotnet\/mcp\/functions\/remote-sample\" target=\"_blank\">\ud83d\udce5 Get the code!<\/a><\/div><\/p>\n<h2>What&#8217;s MCP anyway?<\/h2>\n<p>The Model Context Protocol is nothing more than a specification that makes it easier for AI applications to talk to tooling of some sort.<\/p>\n<p>And generally speaking, that tooling will provide\/expose some core business functionality. Like maybe an API of sorts that stores and returns data from Azure Blob Storage.<\/p>\n<p>So here&#8217;s the situation: you&#8217;re going to have a chat interface of some sort that uses an LLM. How do you get it so that when the LLM is responding to a prompt it knows to invoke the tooling? That&#8217;s where MCP comes in.<\/p>\n<p>But MCP is a spec &#8211; and that means you have to implement it yourself. And plumbing code is no fun. So that&#8217;s what I&#8217;m <em>really<\/em> here to show you today, how to create a remote MCP server using Azure Functions.<\/p>\n<h3>Remote vs local MCP servers<\/h3>\n<p>You may have noticed I&#8217;m being very intentional to specify <em>remote MCP server<\/em>. And there&#8217;s a reason for that.<\/p>\n<p>Right now the most common scenario that involves MCP is a client running locally, like VS Code or Claude Desktop, that has an extension that acts the MCP client (think GitHub Copilot for VS Code) that uses an LLM to call a MCP server also running locally. The MCP server is usually hosted in a Docker container.<\/p>\n<p>But it gets old pretty quickly to install the same MCP server locally everywhere you may need it. Much less making sure people on your team have the same version installed &#8211; it&#8217;s like taking care of a desktop app.<\/p>\n<p>Remote MCP servers run remotely. As long as the endpoint supports server-side events (SSE), you&#8217;re good to go.<\/p>\n<h2>Azure Functions remote MCP servers<\/h2>\n<p>Azure Functions is an event-based serverless product. To me, the defining feature of Functions is its ability to seamlessly integrate with other Azure services just be adding attributes to the function definition.<\/p>\n<p>For example, if you want to write to a blob in Azure Storage just decorate your function definition with <code>[BlobOutput(blobPath)]<\/code> and whatever value you return from the function gets written to the blob specified in <code>blobPath<\/code>.<\/p>\n<p>The Functions team recently released an experimental preview that turns a function app into a MCP Server via a <code>[MCPToolTrigger]<\/code> attribute. So now it&#8217;s amazingly simple to build an MCP server by using the straightforward development experience of Azure Functions <em>and<\/em> you still get all the great Azure integration you&#8217;ve come to expect too!<\/p>\n<h3>Let&#8217;s explore an Azure Functions MCP server<\/h3>\n<p>Instead of doing a file-&gt;new sample, let&#8217;s start from one that&#8217;s already ready to go and explore its defining characteristics. Head over to the <a href=\"https:\/\/aka.ms\/cadotnet\/mcp\/functions\/remote-sample\">Remote MCP Functions Sample<\/a> repo to fork\/clone\/download or just follow along with the code.<\/p>\n<p>This function app lets users highlight text in the editor of VS Code and ask GitHub Copilot to save it with a name. You can then retrieve it using the name you saved it as.<\/p>\n<p>First things first. If you open up the <strong>FunctionsMcpTool.csproj<\/strong> you&#8217;ll see that there&#8217;s a NuGet package called <strong>Microsoft.Azure.Functions.Worker.Extensions.Mcp<\/strong>. This is the one that adds all the MCP-ness to the Function app.<\/p>\n<p>Now checkout <strong>Program.cs<\/strong>. See the line <code>builder.EnableMcpToolMetaData()<\/code>? That&#8217;s going to expose the metadata of each function, like name and description, to the client so the LLM is able to figure out when to invoke it.<\/p>\n<p>Head on over to <strong>SnippetsTool.cs<\/strong>. There are 2 functions here. <strong>SaveSnippet<\/strong> adds text as a blob to Azure Storage. And the other, <strong>GetSnippet<\/strong> returns the text stored in the blob.<\/p>\n<p>Let&#8217;s see how to save some text as a blob:<\/p>\n<pre><code class=\"language-csharp\">[Function(nameof(SaveSnippet))]\r\n[BlobOutput(BlobPath)]\r\npublic string SaveSnippet(\r\n    [McpToolTrigger(SaveSnippetToolName, SaveSnippetToolDescription)]\r\n        ToolInvocationContext context,\r\n    [McpToolProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription)]\r\n        string name,\r\n    [McpToolProperty(SnippetPropertyName, PropertyType, SnippetPropertyDescription)]\r\n        string snippet\r\n)\r\n{\r\n    return snippet;\r\n}<\/code><\/pre>\n<p>Let&#8217;s explain this a bit as there&#8217;s a lot of constants being passed in to the properties.<\/p>\n<ul>\n<li><code>[McpToolTrigger]<\/code>: This defines the function as a something that can be invoked from the MCP client. <code>SaveSnippetToolName<\/code> and <code>SaveSnippetToolDescription<\/code> are constants from the <strong>ToolsInformation.cs<\/strong> that the <code>builder.EnableMcpToolMetaData()<\/code> uses to help the client&#8217;s LLM know when to invoke this function.<\/li>\n<li><code>[McpToolProperty]<\/code>: There are 2 of these in this function. One is for taking in the name of the snippet from the user so it can later be retrieved and the other is of the snippet itself. <code>SnippetNamePropertyName<\/code> and <code>SnippetNamePropertyDescription<\/code> are used as metadata. The <code>PropertyType<\/code> in this case indicates we can expect a <code>string<\/code>.<\/li>\n<\/ul>\n<p>Then because this function is decorated with <code>BlobOutput<\/code> anything we return from it will be written to blob storage. And in this case that is the snippet that was sent from the MCP client.<\/p>\n<p>Cool? Cool.<\/p>\n<p>We return the blob just a little bit differently because we wanted to show off how to do it without using the <code>[McpToolProperty]<\/code> attributes.<\/p>\n<p>Open up <strong>Program.cs<\/strong> again and checkout:<\/p>\n<pre><code class=\"language-csharp\">builder\r\n    .ConfigureMcpTool(GetSnippetToolName)\r\n    .WithProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription);<\/code><\/pre>\n<p>So that&#8217;s saying: for the function defined with the name <code>GetSnippetTool<\/code> (which is a constant in the <strong>ToolsInformation.cs<\/strong>) add a MCP property to its definition. The property is named <code>SnippetNamePropertyName<\/code> has a description of <code>SnippetNamePropertyDescription<\/code> and it&#8217;s a string type.<\/p>\n<p>The function definition looks like:<\/p>\n<pre><code class=\"language-csharp\">[Function(nameof(GetSnippet))]\r\npublic object GetSnippet(\r\n    [McpToolTrigger(GetSnippetToolName, GetSnippetToolDescription)]\r\n        ToolInvocationContext context,\r\n    [BlobInput(BlobPath)] string snippetContent\r\n)\r\n{\r\n    return snippetContent;\r\n}<\/code><\/pre>\n<p>So it&#8217;s returning whatever is in the <code>BlobPath<\/code>. That is defined as:<\/p>\n<pre><code class=\"language-csharp\">private const string BlobPath = \"snippets\/{mcptoolargs.\" + SnippetNamePropertyName + \"}.json\";<\/code><\/pre>\n<p>Well look at that, <code>SnippetNamePropertyName<\/code> makes an appearance. So the path of where the blob is stored within the storage container is defined by its name!<\/p>\n<h3>Deploy to Azure<\/h3>\n<p>The real reason we started with this pre-baked sample is because it&#8217;s super easy to deploy it to Azure thanks to the Azure Developer CLI (azd).<\/p>\n<p>Assuming you have azd already installed. Open up a terminal, change to the base directory of the repository and run:<\/p>\n<pre><code class=\"language-bash\">azd up<\/code><\/pre>\n<p>That&#8217;s it. After asking a couple of questions like which Azure subscription you want to use, a name to use, and which region to deploy in, azd will take care of everything. It will provision all the Azure resources. It will deploy the code. It&#8217;s going to do everything except setup VS Code.<\/p>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>You don&#8217;t need to deploy to Azure! Follow <a href=\"https:\/\/github.com\/Azure-Samples\/remote-mcp-functions-dotnet?tab=readme-ov-file#prepare-your-local-environment\">these steps<\/a> to run the Function app locally with the Functions Core CLI.<\/div><\/p>\n<h2>Consuming the MCP remote server<\/h2>\n<p>We&#8217;re going to use VS Code, and specifically the GitHub Copilot extension, to test out the remote MCP server. (Find out more about <a href=\"https:\/\/docs.github.com\/en\/copilot\/managing-copilot\/managing-copilot-as-an-individual-subscriber\/getting-started-with-copilot-on-your-personal-account\/about-individual-copilot-plans-and-benefits\">Copilot plans<\/a>).<\/p>\n<h3>Grabbing the Functions info<\/h3>\n<p>There are 2 pieces of information we&#8217;ll need about the Azure function we just deployed. The default domain and the system key for the <strong>mcp_extension<\/strong>.<\/p>\n<p>Go to the Azure portal and open the Function app you just deployed with <code>azd up<\/code>. The default domain will be listed in the <strong>Overview<\/strong> tab under the <strong>Essentials<\/strong> section.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/default-domain.png\" alt=\"Screenshot from the Azure portal showing where to get the function's default domain from\" \/><\/p>\n<p>Next open up the <strong>App Keys<\/strong> tab. (It may be easiest to search for it.) And copy the value from the <strong>mcp_extension<\/strong> key.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/app-keys.png\" alt=\"Screenshot from the Azure portal showing where to get the function's mcp_extension system key from.\" \/><\/p>\n<h3>Setting up VS Code<\/h3>\n<p>Open up a brand new instance of VS Code and open or create a .NET project (this way we have some code to save \ud83d\ude09).<\/p>\n<ol>\n<li>In VS Code&#8217;s command palette, type (and select): <code>&gt; MCP: Add Server...<\/code><\/li>\n<li>Next select <code>HTTP (server-sent events)<\/code>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/server-sent-events-picker.png\" alt=\"Screenshot of VS Code's add MCP server dialog for server sent events\" \/><\/li>\n<li>Now you&#8217;ll have to enter the server URL. That&#8217;s going to be <code>https:\/\/{default-function-domain}\/runtime\/webhooks\/mcp\/sse<\/code>. Don&#8217;t forget the <code>\/runtime\/webhooks\/mcp\/sse<\/code> part!\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/server-url.png\" alt=\"Screenshot of VS Code's add MCP server dialog specifying the URL of the function app\" \/><\/li>\n<li>You&#8217;ll get prompted for a local name &#8211; you can use the default one or any name you&#8217;d like.<\/li>\n<li>Then when asked about where you want to save this, pick <code>Workspace<\/code>.\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/workspace.png\" alt=\"Screenshot of VS Code's add MCP server dialog and picking where to save the server\" \/><\/li>\n<li>A file named <strong>mcp.json<\/strong> will be created in the <strong>.vscode<\/strong> folder for you. It will look like this:\n<pre><code class=\"language-json\">{\r\n    \"servers\": {\r\n        \"my-mcp-server-f84232fb\": {\r\n            \"type\": \"sse\",\r\n            \"url\": \"https:\/\/YOUR-DEFAULT-DOMAIN-URL\/runtime\/webhooks\/mcp\/sse\"\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<\/li>\n<li>Almost there! Functions is going to require the <strong>mcp_extension<\/strong> key we copied earlier to be sent in the header. We could hardcode it in, but let&#8217;s instead make VS Code prompt us for it. Update the <strong>mcp.json<\/strong> file so it looks like this:\n<pre><code class=\"language-json\">{\r\n    \"inputs\": [\r\n        {\r\n            \"type\": \"promptString\",\r\n            \"id\": \"functions-mcp-extension-system-key\",\r\n            \"description\": \"Azure Functions MCP Extension System Key\",\r\n            \"password\": true\r\n        }\r\n    ],\r\n    \"servers\": {\r\n        \"my-mcp-server-f84232fb\": {\r\n            \"type\": \"sse\",\r\n            \"url\": \"https:\/\/YOUR-DEFAULT-DOMAIN-URL\/runtime\/webhooks\/mcp\/sse\",\r\n            \"headers\": {\r\n                \"x-functions-key\": \"${input:functions-mcp-extension-system-key}\"\r\n            }\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<\/li>\n<li>There should be a <strong>Start<\/strong> text link right above the server definition. Click it. VS Code will prompt you for the key.\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/prompt-for-key.png\" alt=\"VS Code prompting for the Azure Function's mcp_extension_key\" \/><\/li>\n<li>If all goes well, you should be connected to your Azure Functions remote MCP server and see that you have 3 tools available.\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/running-with-tools.png\" alt=\"VS Code showing a running MCP remote server connected to Azure Functions\" \/><\/li>\n<\/ol>\n<h3>Using the MCP server<\/h3>\n<p>Now for the fun stuff &#8211; getting the LLM to invoke the MCP server (or the tools) just by kinda sorta telling it to.<\/p>\n<p>Open up a code file and then Copilot. Make sure Copilot is set to be in <strong>Agent<\/strong> mode.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/agent-mode.png\" alt=\"GitHub Copilot in Agent mode\" \/><\/p>\n<p>You&#8217;ll notice on top of the text box where you chat with Copilot is a little icon that looks like 2 wrenches. If you click on that a listing of all the tools that Copilot (our MCP client) has access to will appear.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/available-tools.png\" alt=\"Listing of the MCP tools that Copilot has access to\" \/><\/p>\n<p>So highlight some code in the editor. Then in the Copilot chat window say something like:<\/p>\n<blockquote><p>Save the highlighted code and call it best-snippet-in-the-world<\/p><\/blockquote>\n<p>Copilot will start to figure out what to do and it should eventually ask you if you want to run the <strong>save_snippet<\/strong> tool.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/save-snippet.png\" alt=\"Copilot prompting if it should run the save_snippet tool\" \/><\/p>\n<p>Then somewhere else &#8211; a new file or wherever, prompt Copilot with something like the following:<\/p>\n<blockquote><p>Put the best-snippet-in-the-world at the cursor<\/p><\/blockquote>\n<p>Copilot will do running and then prompt if you want to perform the <strong>get_snippets<\/strong> tool. If you say yes, it will put the snippet you saved before where your cursor was!<\/p>\n<h2>Summary<\/h2>\n<p>Adding tooling to LLM-based applications was possible before MCP, but the Model Context Protocol has made it much simpler and also opened the world up to a greater variety of tooling you can add.<\/p>\n<p>Azure Functions is one of those and all it takes is creating a function that&#8217;s a <code>McpToolTrigger<\/code> and away you go.<\/p>\n<p>Don&#8217;t forget to <a href=\"https:\/\/aka.ms\/cadotnet\/mcp\/functions\/remote-sample\">check out the code for this sample<\/a> and watch the complete walkthrough in this video:<\/p>\n<p><iframe width=\"800\" height=\"450\" src=\"https:\/\/www.youtube.com\/embed\/XwnEtZxaokg?si=uP560Gx6GHYJIQy_\" allowfullscreen><\/iframe><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Build AI-powered tools quickly using Azure Functions to create remote MCP servers that seamlessly integrate with GitHub Copilot and other LLM-based applications.<\/p>\n","protected":false},"author":569,"featured_media":56475,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7781,327,7252],"tags":[568,37,8032],"class_list":["post-56474","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-ai","category-azure","category-cloud","tag-ai","tag-azure","tag-mcp"],"acf":[],"blog_post_summary":"<p>Build AI-powered tools quickly using Azure Functions to create remote MCP servers that seamlessly integrate with GitHub Copilot and other LLM-based applications.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/56474","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/569"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=56474"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/56474\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/56475"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=56474"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=56474"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=56474"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}