{"id":1935,"date":"2025-04-16T21:17:39","date_gmt":"2025-04-16T21:17:39","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/04\/16\/build-mcp-remote-servers-with-azure-functions\/"},"modified":"2025-04-16T21:17:39","modified_gmt":"2025-04-16T21:17:39","slug":"build-mcp-remote-servers-with-azure-functions","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/04\/16\/build-mcp-remote-servers-with-azure-functions\/","title":{"rendered":"Build MCP Remote Servers with Azure Functions"},"content":{"rendered":"<p>You can\u2019t 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\u2019m 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\n<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\"> Get the code!<\/a><\/div>\n<h2>What\u2019s 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\u2019s the situation: you\u2019re 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\u2019s where MCP comes in.<\/p>\n<p>But MCP is a spec \u2013 and that means you have to implement it yourself. And plumbing code is no fun. So that\u2019s what I\u2019m <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\u2019m being very intentional to specify <em>remote MCP server<\/em>. And there\u2019s 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 \u2013 it\u2019s 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\u2019re 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 [BlobOutput(blobPath)] and whatever value you return from the function gets written to the blob specified in blobPath.<\/p>\n<p>The Functions team recently released an experimental preview that turns a function app into a MCP Server via a [MCPToolTrigger] attribute. So now it\u2019s 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\u2019ve come to expect too!<\/p>\n<h3>Let\u2019s explore an Azure Functions MCP server<\/h3>\n<p>Instead of doing a file-&gt;new sample, let\u2019s start from one that\u2019s 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\u2019ll see that there\u2019s 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 builder.EnableMcpToolMetaData()? That\u2019s 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\u2019s see how to save some text as a blob:<\/p>\n<p>[Function(nameof(SaveSnippet))]<br \/>\n[BlobOutput(BlobPath)]<br \/>\npublic string SaveSnippet(<br \/>\n    [McpToolTrigger(SaveSnippetToolName, SaveSnippetToolDescription)]<br \/>\n        ToolInvocationContext context,<br \/>\n    [McpToolProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription)]<br \/>\n        string name,<br \/>\n    [McpToolProperty(SnippetPropertyName, PropertyType, SnippetPropertyDescription)]<br \/>\n        string snippet<br \/>\n)<br \/>\n{<br \/>\n    return snippet;<br \/>\n}<\/p>\n<p>Let\u2019s explain this a bit as there\u2019s a lot of constants being passed in to the properties.<\/p>\n<p>[McpToolTrigger]: This defines the function as a something that can be invoked from the MCP client. SaveSnippetToolName and SaveSnippetToolDescription are constants from the <strong>ToolsInformation.cs<\/strong> that the builder.EnableMcpToolMetaData() uses to help the client\u2019s LLM know when to invoke this function.<br \/>\n[McpToolProperty]: 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. SnippetNamePropertyName and SnippetNamePropertyDescription are used as metadata. The PropertyType in this case indicates we can expect a string.<\/p>\n<p>Then because this function is decorated with BlobOutput 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 [McpToolProperty] attributes.<\/p>\n<p>Open up <strong>Program.cs<\/strong> again and checkout:<\/p>\n<p>builder<br \/>\n    .ConfigureMcpTool(GetSnippetToolName)<br \/>\n    .WithProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription);<\/p>\n<p>So that\u2019s saying: for the function defined with the name GetSnippetTool (which is a constant in the <strong>ToolsInformation.cs<\/strong>) add a MCP property to its definition. The property is named SnippetNamePropertyName has a description of SnippetNamePropertyDescription and it\u2019s a string type.<\/p>\n<p>The function definition looks like:<\/p>\n<p>[Function(nameof(GetSnippet))]<br \/>\npublic object GetSnippet(<br \/>\n    [McpToolTrigger(GetSnippetToolName, GetSnippetToolDescription)]<br \/>\n        ToolInvocationContext context,<br \/>\n    [BlobInput(BlobPath)] string snippetContent<br \/>\n)<br \/>\n{<br \/>\n    return snippetContent;<br \/>\n}<\/p>\n<p>So it\u2019s returning whatever is in the BlobPath. That is defined as:<\/p>\n<p>private const string BlobPath = &#8220;snippets\/{mcptoolargs.&#8221; + SnippetNamePropertyName + &#8220;}.json&#8221;;<\/p>\n<p>Well look at that, SnippetNamePropertyName 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\u2019s 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<p>azd up<\/p>\n<p>That\u2019s 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\u2019s going to do everything except setup VS Code.<\/p>\n\n<div class=\"alert alert-primary\">\n<p class=\"alert-divider\"><strong>Note<\/strong><\/p>\n<p>You don\u2019t 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.<\/p><\/div>\n<h2>Consuming the MCP remote server<\/h2>\n<p>We\u2019re 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\u2019ll 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 azd up. The default domain will be listed in the <strong>Overview<\/strong> tab under the <strong>Essentials<\/strong> section.<\/p>\n\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\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 ).<\/p>\n<p>In VS Code\u2019s command palette, type (and select): &gt; MCP: Add Server&#8230;<br \/>\nNext select HTTP (server-sent events)<\/p>\n<p>Now you\u2019ll have to enter the server URL. That\u2019s going to be https:\/\/{default-function-domain}\/runtime\/webhooks\/mcp\/sse. Don\u2019t forget the \/runtime\/webhooks\/mcp\/sse part!<\/p>\n<p>You\u2019ll get prompted for a local name \u2013 you can use the default one or any name you\u2019d like.<br \/>\nThen when asked about where you want to save this, pick Workspace.<\/p>\n<p>A file named <strong>mcp.json<\/strong> will be created in the <strong>.vscode<\/strong> folder for you. It will look like this:<br \/>\n{<br \/>\n    &#8220;servers&#8221;: {<br \/>\n        &#8220;my-mcp-server-f84232fb&#8221;: {<br \/>\n            &#8220;type&#8221;: &#8220;sse&#8221;,<br \/>\n            &#8220;url&#8221;: &#8220;https:\/\/YOUR-DEFAULT-DOMAIN-URL\/runtime\/webhooks\/mcp\/sse&#8221;<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<p>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\u2019s instead make VS Code prompt us for it. Update the <strong>mcp.json<\/strong> file so it looks like this:<br \/>\n{<br \/>\n    &#8220;inputs&#8221;: [<br \/>\n        {<br \/>\n            &#8220;type&#8221;: &#8220;promptString&#8221;,<br \/>\n            &#8220;id&#8221;: &#8220;functions-mcp-extension-system-key&#8221;,<br \/>\n            &#8220;description&#8221;: &#8220;Azure Functions MCP Extension System Key&#8221;,<br \/>\n            &#8220;password&#8221;: true<br \/>\n        }<br \/>\n    ],<br \/>\n    &#8220;servers&#8221;: {<br \/>\n        &#8220;my-mcp-server-f84232fb&#8221;: {<br \/>\n            &#8220;type&#8221;: &#8220;sse&#8221;,<br \/>\n            &#8220;url&#8221;: &#8220;https:\/\/YOUR-DEFAULT-DOMAIN-URL\/runtime\/webhooks\/mcp\/sse&#8221;,<br \/>\n            &#8220;headers&#8221;: {<br \/>\n                &#8220;x-functions-key&#8221;: &#8220;${input:functions-mcp-extension-system-key}&#8221;<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<p>There should be a <strong>Start<\/strong> text link right above the server definition. Click it. VS Code will prompt you for the key.<\/p>\n<p>If all goes well, you should be connected to your Azure Functions remote MCP server and see that you have 3 tools available.<\/p>\n<h3>Using the MCP server<\/h3>\n<p>Now for the fun stuff \u2013 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\n<p>You\u2019ll 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\n<p>So highlight some code in the editor. Then in the Copilot chat window say something like:<\/p>\n<p>Save the highlighted code and call it best-snippet-in-the-world<\/p>\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\n<p>Then somewhere else \u2013 a new file or wherever, prompt Copilot with something like the following:<\/p>\n<p>Put the best-snippet-in-the-world at the cursor<\/p>\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\u2019s a McpToolTrigger and away you go.<\/p>\n<p>Don\u2019t 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\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/build-mcp-remote-servers-with-azure-functions\/\">Build MCP Remote Servers with Azure Functions<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\">.NET Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>You can\u2019t run to the grocery store these days without hearing about the Model Context Protocol (MCP)! Well, I hope [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[7],"tags":[],"class_list":["post-1935","post","type-post","status-publish","format-standard","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1935","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=1935"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1935\/revisions"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=1935"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=1935"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=1935"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}