Expose workflow as MCP Tool

This commit is contained in:
Shyju Krishnankutty
2026-03-18 14:01:35 -07:00
Unverified
parent d3d0100822
commit 3ab6d7d686
18 changed files with 546 additions and 1 deletions
@@ -5,6 +5,8 @@ using System.Reflection;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests;
/// <summary>
@@ -235,6 +237,60 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) :
});
}
[Fact]
public async Task WorkflowMcpToolSampleValidationAsync()
{
string samplePath = Path.Combine(s_samplesPath, "04_WorkflowMcpTool");
await this.RunSampleTestAsync(samplePath, requiresOpenAI: false, async (logs) =>
{
// Connect to the MCP endpoint exposed by the Azure Functions host
IClientTransport clientTransport = new HttpClientTransport(new()
{
Endpoint = new Uri($"http://localhost:{AzureFunctionsPort}/runtime/webhooks/mcp")
});
await using McpClient mcpClient = await McpClient.CreateAsync(clientTransport);
// Verify both workflow tools are listed
IList<McpClientTool> tools = await mcpClient.ListToolsAsync();
this._outputHelper.WriteLine($"MCP tools found: {string.Join(", ", tools.Select(t => t.Name))}");
Assert.Single(tools, t => t.Name == "Translate");
Assert.Single(tools, t => t.Name == "OrderLookup");
// Invoke the Translate workflow via MCP tool (returns a string result)
this._outputHelper.WriteLine("Invoking MCP tool 'Translate'...");
CallToolResult translateResult = await mcpClient.CallToolAsync(
"Translate",
arguments: new Dictionary<string, object?> { { "input", "hello world" } });
string translateResponse = ((TextContentBlock)translateResult.Content[0]).Text;
this._outputHelper.WriteLine($"Translate MCP tool response: {translateResponse}");
Assert.NotEmpty(translateResponse);
Assert.Contains("HELLO WORLD", translateResponse);
// Invoke the OrderLookup workflow via MCP tool (returns a POCO serialized as JSON)
this._outputHelper.WriteLine("Invoking MCP tool 'OrderLookup'...");
CallToolResult orderResult = await mcpClient.CallToolAsync(
"OrderLookup",
arguments: new Dictionary<string, object?> { { "input", "ORD-2025-42" } });
string orderResponse = ((TextContentBlock)orderResult.Content[0]).Text;
this._outputHelper.WriteLine($"OrderLookup MCP tool response: {orderResponse}");
Assert.NotEmpty(orderResponse);
Assert.Contains("ORD-2025-42", orderResponse);
// Verify executor activities ran in the logs
lock (logs)
{
Assert.True(logs.Any(log => log.Message.Contains("[Activity] TranslateText:")), "TranslateText activity not found in logs.");
Assert.True(logs.Any(log => log.Message.Contains("[Activity] FormatOutput:")), "FormatOutput activity not found in logs.");
Assert.True(logs.Any(log => log.Message.Contains("[Activity] LookupOrder:")), "LookupOrder activity not found in logs.");
Assert.True(logs.Any(log => log.Message.Contains("[Activity] EnrichOrder:")), "EnrichOrder activity not found in logs.");
}
});
}
[Fact]
public async Task ConcurrentWorkflowSampleValidationAsync()
{
@@ -0,0 +1,114 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests;
public sealed class FunctionMetadataFactoryTests
{
[Fact]
public void CreateEntityTrigger_SetsCorrectNameAndBindings()
{
DefaultFunctionMetadata metadata = FunctionMetadataFactory.CreateEntityTrigger("myAgent");
Assert.Equal("dafx-myAgent", metadata.Name);
Assert.Equal("dotnet-isolated", metadata.Language);
Assert.Equal(BuiltInFunctions.RunAgentEntityFunctionEntryPoint, metadata.EntryPoint);
Assert.NotNull(metadata.RawBindings);
Assert.Equal(2, metadata.RawBindings.Count);
Assert.Contains("entityTrigger", metadata.RawBindings[0]);
Assert.Contains("durableClient", metadata.RawBindings[1]);
}
[Fact]
public void CreateHttpTrigger_SetsCorrectNameRouteAndDefaults()
{
DefaultFunctionMetadata metadata = FunctionMetadataFactory.CreateHttpTrigger(
"myWorkflow", "workflows/myWorkflow/run", BuiltInFunctions.RunWorkflowOrchestrationHttpFunctionEntryPoint);
Assert.Equal("http-myWorkflow", metadata.Name);
Assert.Equal("dotnet-isolated", metadata.Language);
Assert.Equal(BuiltInFunctions.RunWorkflowOrchestrationHttpFunctionEntryPoint, metadata.EntryPoint);
Assert.NotNull(metadata.RawBindings);
Assert.Equal(3, metadata.RawBindings.Count);
Assert.Contains("httpTrigger", metadata.RawBindings[0]);
Assert.Contains("workflows/myWorkflow/run", metadata.RawBindings[0]);
Assert.Contains("\"post\"", metadata.RawBindings[0]);
Assert.Contains("http", metadata.RawBindings[1]);
Assert.Contains("durableClient", metadata.RawBindings[2]);
}
[Fact]
public void CreateHttpTrigger_RespectsCustomMethods()
{
DefaultFunctionMetadata metadata = FunctionMetadataFactory.CreateHttpTrigger(
"status", "workflows/status/{runId}", BuiltInFunctions.GetWorkflowStatusHttpFunctionEntryPoint, methods: "\"get\"");
Assert.NotNull(metadata.RawBindings);
Assert.Contains("\"get\"", metadata.RawBindings[0]);
Assert.DoesNotContain("\"post\"", metadata.RawBindings[0]);
}
[Fact]
public void CreateActivityTrigger_SetsCorrectNameAndBindings()
{
DefaultFunctionMetadata metadata = FunctionMetadataFactory.CreateActivityTrigger("dafx-MyExecutor");
Assert.Equal("dafx-MyExecutor", metadata.Name);
Assert.Equal("dotnet-isolated", metadata.Language);
Assert.Equal(BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, metadata.EntryPoint);
Assert.NotNull(metadata.RawBindings);
Assert.Equal(2, metadata.RawBindings.Count);
Assert.Contains("activityTrigger", metadata.RawBindings[0]);
Assert.Contains("durableClient", metadata.RawBindings[1]);
}
[Fact]
public void CreateOrchestrationTrigger_SetsCorrectNameAndBindings()
{
DefaultFunctionMetadata metadata = FunctionMetadataFactory.CreateOrchestrationTrigger(
"dafx-MyWorkflow", BuiltInFunctions.RunWorkflowOrchestrationFunctionEntryPoint);
Assert.Equal("dafx-MyWorkflow", metadata.Name);
Assert.Equal("dotnet-isolated", metadata.Language);
Assert.Equal(BuiltInFunctions.RunWorkflowOrchestrationFunctionEntryPoint, metadata.EntryPoint);
Assert.NotNull(metadata.RawBindings);
Assert.Single(metadata.RawBindings);
Assert.Contains("orchestrationTrigger", metadata.RawBindings[0]);
}
[Fact]
public void CreateWorkflowMcpToolTrigger_SetsCorrectNameAndBindings()
{
DefaultFunctionMetadata metadata = FunctionMetadataFactory.CreateWorkflowMcpToolTrigger("Translate", "Translate text");
Assert.Equal("mcptool-Translate", metadata.Name);
Assert.Equal("dotnet-isolated", metadata.Language);
Assert.Equal(BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint, metadata.EntryPoint);
Assert.NotNull(metadata.RawBindings);
Assert.Equal(3, metadata.RawBindings.Count);
// mcpToolTrigger binding
Assert.Contains("mcpToolTrigger", metadata.RawBindings[0]);
Assert.Contains("\"toolName\":\"Translate\"", metadata.RawBindings[0]);
Assert.Contains("\"description\":\"Translate text\"", metadata.RawBindings[0]);
Assert.Contains("toolProperties", metadata.RawBindings[0]);
// mcpToolProperty binding for input
Assert.Contains("mcpToolProperty", metadata.RawBindings[1]);
Assert.Contains("\"propertyName\":\"input\"", metadata.RawBindings[1]);
Assert.Contains("\"isRequired\":true", metadata.RawBindings[1]);
// durableClient binding
Assert.Contains("durableClient", metadata.RawBindings[2]);
}
[Fact]
public void CreateWorkflowMcpToolTrigger_UsesDefaultDescription_WhenNull()
{
DefaultFunctionMetadata metadata = FunctionMetadataFactory.CreateWorkflowMcpToolTrigger("MyWorkflow", description: null);
Assert.NotNull(metadata.RawBindings);
Assert.Contains("Run the MyWorkflow workflow", metadata.RawBindings[0]);
}
}