diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/README.md b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/README.md index f1a3afa5fa..a5411bf375 100644 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/README.md +++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/README.md @@ -24,7 +24,7 @@ The sample creates two workflows exposed as MCP tools: | Executor | Input | Output | Description | |----------|-------|--------|-------------| | **LookupOrder** | `string` | `OrderInfo` | Looks up an order by ID | -| **EnrichOrder** | `OrderInfo` | `OrderSummary` | Adds computed fields (total price, delivery estimate) | +| **EnrichOrder** | `OrderInfo` | `OrderSummary` | Adds computed fields (total price, status) | ## Environment Setup @@ -76,6 +76,6 @@ For this sample, you'll also need [Node.js](https://nodejs.org/en/download) to u - Select the `OrderLookup` tool - Set `ORD-2025-42` as the `input` parameter - Click **Run Tool** - - Expected result: A JSON object containing order details such as `OrderId`, `CustomerName`, `Product`, `TotalPrice`, `EstimatedDelivery`, and `Status` + - Expected result: A JSON object containing order details such as `OrderId`, `CustomerName`, `Product`, `TotalPrice`, and `Status` You'll see the workflow executor activities logged in the terminal where you ran `func start`. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs index 4b459daad8..3420c56512 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -409,7 +409,22 @@ internal static class BuiltInFunctions getInputsAndOutputs: true, cancellation: functionContext.CancellationToken); - return metadata?.ReadOutputAs()?.Result; + if (metadata is null) + { + throw new InvalidOperationException($"Workflow orchestration '{instanceId}' returned no metadata."); + } + + if (metadata.RuntimeStatus is OrchestrationRuntimeStatus.Failed) + { + throw new InvalidOperationException($"Workflow orchestration '{instanceId}' failed: {metadata.ReadOutputAs()}"); + } + + if (metadata.RuntimeStatus is not OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Workflow orchestration '{instanceId}' ended with unexpected status '{metadata.RuntimeStatus}'."); + } + + return metadata.ReadOutputAs()?.Result; } /// diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionMetadataFactory.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionMetadataFactory.cs index 43dd53be18..46053507b1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionMetadataFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionMetadataFactory.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json.Nodes; using Microsoft.Agents.AI.DurableTask; using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata; @@ -112,28 +113,49 @@ internal static class FunctionMetadataFactory var functionName = $"{BuiltInFunctions.McpToolPrefix}{workflowName}"; var toolDescription = description ?? $"Run the {workflowName} workflow"; - const string ToolProperties = - """[{\"propertyName\":\"input\",\"propertyType\":\"string\",\"description\":\"The input to the workflow.\",\"isRequired\":true,\"isArray\":false}]"""; + var toolProperties = new JsonArray(new JsonObject + { + ["propertyName"] = "input", + ["propertyType"] = "string", + ["description"] = "The input to the workflow.", + ["isRequired"] = true, + ["isArray"] = false, + }); - var TriggerBinding = - $$"""{"name":"context","type":"mcpToolTrigger","direction":"In","toolName":"{{workflowName}}","description":"{{toolDescription}}","toolProperties":"{{ToolProperties}}"}"""; + var triggerBinding = new JsonObject + { + ["name"] = "context", + ["type"] = "mcpToolTrigger", + ["direction"] = "In", + ["toolName"] = workflowName, + ["description"] = toolDescription, + ["toolProperties"] = toolProperties.ToJsonString(), + }; - const string InputBinding = - """{"name":"input","type":"mcpToolProperty","direction":"In","propertyName":"input","description":"The input to the workflow","isRequired":true,"dataType":"String","propertyType":"string"}"""; + var inputBinding = new JsonObject + { + ["name"] = "input", + ["type"] = "mcpToolProperty", + ["direction"] = "In", + ["propertyName"] = "input", + ["description"] = "The input to the workflow", + ["isRequired"] = true, + ["dataType"] = "String", + ["propertyType"] = "string", + }; - const string ClientBinding = - """{"name":"client","type":"durableClient","direction":"In"}"""; + var clientBinding = new JsonObject + { + ["name"] = "client", + ["type"] = "durableClient", + ["direction"] = "In", + }; return new DefaultFunctionMetadata { Name = functionName, Language = "dotnet-isolated", - RawBindings = - [ - TriggerBinding, - InputBinding, - ClientBinding - ], + RawBindings = [triggerBinding.ToJsonString(), inputBinding.ToJsonString(), clientBinding.ToJsonString()], EntryPoint = BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint, ScriptFile = BuiltInFunctions.ScriptFile, }; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowOptionsExtensions.cs index 08ecb79d6d..7cf38397ae 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowOptionsExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowOptionsExtensions.cs @@ -29,7 +29,7 @@ public static class DurableWorkflowOptionsExtensions } /// - /// Adds a workflow and optionally exposes a status HTTP endpoint and/or an MCP tool trigger. + /// Adds a workflow and configures whether to expose a status HTTP endpoint and/or an MCP tool trigger. /// /// The workflow options to add the workflow to. /// The workflow instance to add. diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs index df2f67944d..64e35ecc88 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs @@ -264,7 +264,8 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) : "Translate", arguments: new Dictionary { { "input", "hello world" } }); - string translateResponse = ((TextContentBlock)translateResult.Content[0]).Text; + Assert.NotEmpty(translateResult.Content); + string translateResponse = Assert.IsType(translateResult.Content[0]).Text; this._outputHelper.WriteLine($"Translate MCP tool response: {translateResponse}"); Assert.NotEmpty(translateResponse); Assert.Contains("HELLO WORLD", translateResponse); @@ -275,7 +276,8 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) : "OrderLookup", arguments: new Dictionary { { "input", "ORD-2025-42" } }); - string orderResponse = ((TextContentBlock)orderResult.Content[0]).Text; + Assert.NotEmpty(orderResult.Content); + string orderResponse = Assert.IsType(orderResult.Content[0]).Text; this._outputHelper.WriteLine($"OrderLookup MCP tool response: {orderResponse}"); Assert.NotEmpty(orderResponse); Assert.Contains("ORD-2025-42", orderResponse); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/FunctionMetadataFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/FunctionMetadataFactoryTests.cs index 60be79d61d..a777c69480 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/FunctionMetadataFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/FunctionMetadataFactoryTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata; namespace Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests; @@ -88,6 +89,12 @@ public sealed class FunctionMetadataFactoryTests Assert.NotNull(metadata.RawBindings); Assert.Equal(3, metadata.RawBindings.Count); + // Verify all bindings are valid JSON + foreach (string binding in metadata.RawBindings) + { + JsonDocument.Parse(binding); + } + // mcpToolTrigger binding Assert.Contains("mcpToolTrigger", metadata.RawBindings[0]); Assert.Contains("\"toolName\":\"Translate\"", metadata.RawBindings[0]);