{"id":3937,"date":"2026-07-16T08:00:10","date_gmt":"2026-07-16T15:00:10","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-sdk\/?p=3937"},"modified":"2026-07-15T15:07:52","modified_gmt":"2026-07-15T22:07:52","slug":"long-running-mcp-tools-azure-functions","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-sdk\/long-running-mcp-tools-azure-functions\/","title":{"rendered":"How to build long-running MCP tools on Azure Functions"},"content":{"rendered":"<p>Recently, a customer building servers with the Azure Functions Model Context Protocol (MCP) extension reached out and asked: <em>How do I handle tools that take longer than the client is willing to wait?<\/em> This becomes especially relevant when tool calls move beyond simple request\/response into multi-step workflows and long-running operations.<\/p>\n<p>At the same time, MCP is evolving to address exactly this. The <a href=\"https:\/\/blog.modelcontextprotocol.io\/posts\/2026-07-28-release-candidate\/#tasks-graduates-to-an-extension\">Tasks extension<\/a> is introduced in the <a href=\"https:\/\/blog.modelcontextprotocol.io\/posts\/2026-07-28-release-candidate\/\">2026-07-28 release candidate<\/a>, defining a standard way to model long-running work.<\/p>\n<p>In this post, we&#8217;ll walk through how to build long-running MCP tools on Azure Functions using <a href=\"https:\/\/learn.microsoft.com\/azure\/durable-task\/durable-functions\/durable-functions-overview?tabs=csharp\">Durable Functions<\/a>, a framework for authoring stateful, long-running workflows as ordinary code, with checkpointing, scaling, and recovery handled automatically.<\/p>\n<h2>MCP tools today<\/h2>\n<p>Today, MCP tools are fundamentally request\/response:<\/p>\n<ul>\n<li>the client issues a <code>tools\/call<\/code><\/li>\n<li>the server returns a result<\/li>\n<\/ul>\n<p>This works well for fast operations, but breaks down when:<\/p>\n<ul>\n<li>workflows take minutes<\/li>\n<li>execution depends on multiple steps<\/li>\n<li>latency is unpredictable<\/li>\n<\/ul>\n<p>In practice, clients enforce their own tool-call timeouts. These aren&#8217;t standardized by the MCP spec and vary per client, but they&#8217;re often in the ~30\u201360 second range. If a tool exceeds that window:<\/p>\n<ul>\n<li>the client times out<\/li>\n<li>the agent observes a failed call<\/li>\n<li>the underlying work may still be running<\/li>\n<\/ul>\n<p>So the core issue is that synchronous tool calls don&#8217;t naturally model long-running work.<\/p>\n<h2>The MCP Tasks extension<\/h2>\n<p>The Tasks extension addresses this. With the extension, a server can respond to a <code>tools\/call<\/code> with an asynchronous <em>task handle<\/em> instead of a final result, and the client drives the lifecycle from there:<\/p>\n<ul>\n<li><code>tasks\/get<\/code>: poll the task&#8217;s status<\/li>\n<li><code>tasks\/update<\/code>: submit input back to the server if the task reaches <code>input_required<\/code><\/li>\n<li><code>tasks\/cancel<\/code>: cancel an in-flight task<\/li>\n<\/ul>\n<p>A task carries a status (<code>working<\/code>, <code>input_required<\/code>, <code>completed<\/code>, <code>failed<\/code>, or <code>cancelled<\/code>) and, on completion, the final result. Task creation is server-directed, meaning the client advertises support by including the extension in its per-request capabilities, and the server decides per request whether to return a task. A server won&#8217;t return a task to a client that hasn&#8217;t advertised support.<\/p>\n<p>It&#8217;s important to note that Tasks rely on ecosystem support. Clients must advertise the extension, and MCP Software Development Kits (SDKs) must implement the task lifecycle, before servers can use it. So while Tasks is now a defined extension, broad client and SDK support is still in progress.<\/p>\n<h2>Implement long-running tasks with Durable Functions today<\/h2>\n<p>Until the Tasks extension is broadly supported across clients, we need a pattern that works with existing request\/response clients and supports long-running execution. The following samples show how, using Durable Functions:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/Azure-Samples\/mcp-functions-long-running-tools-python\">Python<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Azure-Samples\/mcp-functions-long-running-tools\">.NET<\/a><\/li>\n<\/ul>\n<p>The long-running work in this sample mines a short chain of blocks. Each block requires solving a computational puzzle where the system keeps trying different inputs until it finds one that produces a result matching a specific pattern (for example, starting with a certain number of zeros). Because this involves lots of trial and error, it naturally takes time, making it a good example of a long-running workflow.<\/p>\n<p>The server in the sample exposes two tools:<\/p>\n<ul>\n<li><strong><code>start_mining<\/code><\/strong>\n<ul>\n<li>Starts a Durable Functions orchestration to mine the blocks<\/li>\n<li>Waits briefly (within a configurable budget)<\/li>\n<li>Returns the result inline if completed within budget, or returns a <code>workflow_id<\/code> if still running<\/li>\n<\/ul>\n<\/li>\n<li><strong><code>get_mining_result<\/code><\/strong>\n<ul>\n<li>Takes the <code>workflow_id<\/code><\/li>\n<li>Returns the current state, for example <code>completed<\/code>, <code>running<\/code>, <code>failed<\/code>, or <code>not_found<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>To ensure that the agent calls the tools in the right order, <code>workflow_id<\/code> is a required parameter of <code>get_mining_result<\/code>, so the agent can&#8217;t poll without starting a mining run first. Also, the <code>running<\/code> response carries a <code>poll_after_seconds<\/code> value and a <code>next<\/code> instruction, prompting the agent to poll again if work isn&#8217;t done rather than give up or assume completion.<\/p>\n<p>Even so, the poll path still relies on the agent correctly remembering, and not hallucinating, the <code>workflow_id<\/code> it was handed. If it garbles or invents an id, the poll lands on the wrong instance or none at all (which is why <code>get_mining_result<\/code> returns <code>not_found<\/code> rather than guessing).<\/p>\n<h2>What changes with the Tasks extension<\/h2>\n<p>Once the Tasks extension is fully implemented across clients and SDKs, the model becomes simpler and more reliable; the server returns a Task handle, the client manages the polling and lifecycle calls, and the SDK tracks execution state. This removes a key limitation of today&#8217;s solution, which requires the agent to remember and correctly pass identifiers like <code>workflow_id<\/code>.<\/p>\n<h2>Call to action<\/h2>\n<p>Try out the sample and let us know whether it addresses your MCP needs around long-running or workflow-type tools!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to build long-running MCP tools on Azure Functions using Durable Functions. This post explains why synchronous tool calls break down for long-running work, introduces the MCP Tasks extension, and walks through a sample pattern that works with existing request\/response clients today.<\/p>\n","protected":false},"author":42940,"featured_media":3939,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[967,734,765,161,988,940,959,162,960],"class_list":["post-3937","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure-sdk","tag-ai-agents","tag-azure","tag-azure-functions","tag-dotnet","tag-durable-functions","tag-mcp","tag-model-context-protocol","tag-python","tag-serverless"],"acf":[],"blog_post_summary":"<p>Learn how to build long-running MCP tools on Azure Functions using Durable Functions. This post explains why synchronous tool calls break down for long-running work, introduces the MCP Tasks extension, and walks through a sample pattern that works with existing request\/response clients today.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3937","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/users\/42940"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/comments?post=3937"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3937\/revisions"}],"predecessor-version":[{"id":3941,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3937\/revisions\/3941"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/media\/3939"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/media?parent=3937"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/categories?post=3937"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/tags?post=3937"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}