Expose workflow as MCP Tool

This commit is contained in:
Shyju Krishnankutty
2026-03-18 14:05:56 -07:00
parent d3d0100822
commit 3ab6d7d686
18 changed files with 546 additions and 1 deletions
@@ -167,6 +167,18 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
return;
}
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint)
{
if (mcpToolInvocationContext is null)
{
throw new InvalidOperationException($"MCP tool invocation context binding is missing for the invocation {context.InvocationId}.");
}
context.GetInvocationResult().Value =
await BuiltInFunctions.RunWorkflowMcpToolAsync(mcpToolInvocationContext, durableTaskClient, context);
return;
}
throw new InvalidOperationException($"Unsupported function entry point '{context.FunctionDefinition.EntryPoint}' for invocation {context.InvocationId}.");
}
@@ -29,6 +29,7 @@ internal static class BuiltInFunctions
internal static readonly string InvokeWorkflowActivityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeWorkflowActivityAsync)}";
internal static readonly string GetWorkflowStatusHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(GetWorkflowStatusAsync)}";
internal static readonly string RespondToWorkflowHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RespondToWorkflowAsync)}";
internal static readonly string RunWorkflowMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunWorkflowMcpToolAsync)}";
#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - Azure Functions does not use single-file publishing
internal static readonly string ScriptFile = Path.GetFileName(typeof(BuiltInFunctions).Assembly.Location);
@@ -378,6 +379,39 @@ internal static class BuiltInFunctions
return agentResponse.Text;
}
/// <summary>
/// Runs a workflow via MCP tool trigger.
/// Extracts the <c>input</c> argument, schedules a new orchestration, waits for completion, and returns the output.
/// </summary>
public static async Task<string?> RunWorkflowMcpToolAsync(
[McpToolTrigger("BuiltInWorkflowMcpTool")] ToolInvocationContext context,
[DurableClient] DurableTaskClient client,
FunctionContext functionContext)
{
if (context.Arguments is null)
{
throw new ArgumentException("MCP Tool invocation is missing required arguments.");
}
if (!context.Arguments.TryGetValue("input", out object? inputObj) || inputObj is not string input)
{
throw new ArgumentException("MCP Tool invocation is missing required 'input' argument of type string.");
}
string workflowName = context.Name;
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
DurableWorkflowInput<string> orchestrationInput = new() { Input = input };
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, orchestrationInput);
OrchestrationMetadata? metadata = await client.WaitForInstanceCompletionAsync(
instanceId,
getInputsAndOutputs: true,
cancellation: functionContext.CancellationToken);
return metadata?.ReadOutputAs<DurableWorkflowResult>()?.Result;
}
/// <summary>
/// Creates an error response with the specified status code and error message.
/// </summary>
@@ -2,6 +2,7 @@
## [Unreleased]
- Added MCP tool trigger support for durable workflows
- Added Azure Functions hosting support for durable workflows ([#4436](https://github.com/microsoft/agent-framework/pull/4436))
## v1.0.0-preview.251219.1
@@ -98,4 +98,27 @@ internal static class FunctionMetadataFactory
ScriptFile = BuiltInFunctions.ScriptFile,
};
}
/// <summary>
/// Creates function metadata for an MCP tool trigger function that starts a workflow.
/// </summary>
/// <param name="workflowName">The name of the workflow to expose as an MCP tool.</param>
/// <param name="description">An optional description for the MCP tool. If null, a default description is generated.</param>
/// <returns>A <see cref="DefaultFunctionMetadata"/> configured for an MCP tool trigger.</returns>
internal static DefaultFunctionMetadata CreateWorkflowMcpToolTrigger(string workflowName, string? description)
{
return new DefaultFunctionMetadata
{
Name = $"{BuiltInFunctions.McpToolPrefix}{workflowName}",
Language = "dotnet-isolated",
RawBindings =
[
$$"""{"name":"context","type":"mcpToolTrigger","direction":"In","toolName":"{{workflowName}}","description":"{{description ?? $"Run the {workflowName} workflow"}}","toolProperties":"[{\"propertyName\":\"input\",\"propertyType\":\"string\",\"description\":\"The input to the workflow.\",\"isRequired\":true,\"isArray\":false}]"}""",
"""{"name":"input","type":"mcpToolProperty","direction":"In","propertyName":"input","description":"The input to the workflow","isRequired":true,"dataType":"String","propertyType":"string"}""",
"""{"name":"client","type":"durableClient","direction":"In"}"""
],
EntryPoint = BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint,
ScriptFile = BuiltInFunctions.ScriptFile,
};
}
}
@@ -107,7 +107,8 @@ public static class FunctionsApplicationBuilderExtensions
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.GetWorkflowStatusHttpFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RespondToWorkflowHttpFunctionEntryPoint, StringComparison.Ordinal)
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RespondToWorkflowHttpFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint, StringComparison.Ordinal)
);
builder.Services.TryAddSingleton<BuiltInFunctionExecutor>();
}
@@ -10,6 +10,7 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
internal sealed class FunctionsDurableOptions : DurableOptions
{
private readonly HashSet<string> _statusEndpointWorkflows = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _mcpToolTriggerWorkflows = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Enables the status HTTP endpoint for the specified workflow.
@@ -26,4 +27,20 @@ internal sealed class FunctionsDurableOptions : DurableOptions
{
return this._statusEndpointWorkflows.Contains(workflowName);
}
/// <summary>
/// Enables the MCP tool trigger for the specified workflow.
/// </summary>
internal void EnableMcpToolTrigger(string workflowName)
{
this._mcpToolTriggerWorkflows.Add(workflowName);
}
/// <summary>
/// Returns whether the MCP tool trigger is enabled for the specified workflow.
/// </summary>
internal bool IsMcpToolTriggerEnabled(string workflowName)
{
return this._mcpToolTriggerWorkflows.Contains(workflowName);
}
}
@@ -27,4 +27,31 @@ public static class DurableWorkflowOptionsExtensions
functionsOptions.EnableStatusEndpoint(workflow.Name!);
}
}
/// <summary>
/// Adds a workflow and optionally exposes a status HTTP endpoint and/or an MCP tool trigger.
/// </summary>
/// <param name="options">The workflow options to add the workflow to.</param>
/// <param name="workflow">The workflow instance to add.</param>
/// <param name="exposeStatusEndpoint">If <see langword="true"/>, a GET endpoint is generated at <c>workflows/{name}/status/{runId}</c>.</param>
/// <param name="exposeMcpToolTrigger">If <see langword="true"/>, an MCP tool trigger is generated for the workflow.</param>
public static void AddWorkflow(this DurableWorkflowOptions options, Workflow workflow, bool exposeStatusEndpoint, bool exposeMcpToolTrigger)
{
ArgumentNullException.ThrowIfNull(options);
options.AddWorkflow(workflow);
if (options.ParentOptions is FunctionsDurableOptions functionsOptions)
{
if (exposeStatusEndpoint)
{
functionsOptions.EnableStatusEndpoint(workflow.Name!);
}
if (exposeMcpToolTrigger)
{
functionsOptions.EnableMcpToolTrigger(workflow.Name!);
}
}
}
}
@@ -113,6 +113,17 @@ internal sealed class DurableWorkflowsFunctionMetadataTransformer : IFunctionMet
}
}
// Register an MCP tool trigger if opted in via AddWorkflow(exposeMcpToolTrigger: true).
if (this._options.IsMcpToolTriggerEnabled(workflow.Key))
{
string mcpToolFunctionName = $"{BuiltInFunctions.McpToolPrefix}{workflow.Key}";
if (registeredFunctions.Add(mcpToolFunctionName))
{
this._logger.LogRegisteringWorkflowTrigger(workflow.Key, mcpToolFunctionName, "mcpTool");
original.Add(FunctionMetadataFactory.CreateWorkflowMcpToolTrigger(workflow.Key, workflow.Value.Description));
}
}
// Register activity or entity functions for each executor in the workflow.
// ReflectExecutors() returns all executors across the graph; no need to manually traverse edges.
foreach (KeyValuePair<string, ExecutorBinding> entry in workflow.Value.ReflectExecutors())