diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 576d2c5c54..ff815348c7 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -76,6 +76,8 @@
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj
new file mode 100644
index 0000000000..68f9ccb801
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj
@@ -0,0 +1,35 @@
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+ WorkflowMcpTool
+ WorkflowMcpTool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/Executors.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/Executors.cs
new file mode 100644
index 0000000000..0621680e33
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/Executors.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowMcpTool;
+
+internal sealed class TranslateText() : Executor("TranslateText")
+{
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($"[Activity] TranslateText: '{message}'");
+ return ValueTask.FromResult(new TranslationResult(message, message.ToUpperInvariant()));
+ }
+}
+
+internal sealed class FormatOutput() : Executor("FormatOutput")
+{
+ public override ValueTask HandleAsync(
+ TranslationResult message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine("[Activity] FormatOutput: Formatting result");
+ return ValueTask.FromResult($"Original: {message.Original} => Translated: {message.Translated}");
+ }
+}
+
+internal sealed class LookupOrder() : Executor("LookupOrder")
+{
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($"[Activity] LookupOrder: '{message}'");
+ return ValueTask.FromResult(new OrderInfo(message, "Alice Johnson", "Wireless Headphones", Quantity: 2, UnitPrice: 49.99m));
+ }
+}
+
+internal sealed class EnrichOrder() : Executor("EnrichOrder")
+{
+ public override ValueTask HandleAsync(
+ OrderInfo message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($"[Activity] EnrichOrder: '{message.OrderId}'");
+ return ValueTask.FromResult(new OrderSummary(message, TotalPrice: message.Quantity * message.UnitPrice, Status: "Confirmed"));
+ }
+}
+
+internal sealed record TranslationResult(string Original, string Translated);
+
+internal sealed record OrderInfo(string OrderId, string CustomerName, string Product, int Quantity, decimal UnitPrice);
+
+internal sealed record OrderSummary(OrderInfo Order, decimal TotalPrice, string Status);
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/Program.cs
new file mode 100644
index 0000000000..0970ca16b8
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/Program.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to expose a durable workflow as an MCP (Model Context Protocol) tool.
+// When using AddWorkflow with exposeMcpToolTrigger: true, the Functions host will automatically
+// generate a remote MCP endpoint for the app at /runtime/webhooks/mcp with a workflow-specific
+// tool name. MCP-compatible clients can then invoke the workflow as a tool.
+
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+using WorkflowMcpTool;
+
+// Define executors
+TranslateText translateText = new();
+FormatOutput formatOutput = new();
+LookupOrder lookupOrder = new();
+EnrichOrder enrichOrder = new();
+
+// Build a simple workflow: TranslateText -> FormatOutput
+Workflow translateWorkflow = new WorkflowBuilder(translateText)
+ .WithName("Translate")
+ .WithDescription("Translate text to uppercase and format the result")
+ .AddEdge(translateText, formatOutput)
+ .Build();
+
+// Build a workflow that returns a POCO: LookupOrder -> EnrichOrder
+Workflow orderLookupWorkflow = new WorkflowBuilder(lookupOrder)
+ .WithName("OrderLookup")
+ .WithDescription("Look up an order by ID and return enriched order details")
+ .AddEdge(lookupOrder, enrichOrder)
+ .Build();
+
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableWorkflows(workflows =>
+ {
+ // Expose both workflows as MCP tool triggers.
+ workflows.AddWorkflow(translateWorkflow, exposeStatusEndpoint: false, exposeMcpToolTrigger: true);
+ workflows.AddWorkflow(orderLookupWorkflow, exposeStatusEndpoint: false, exposeMcpToolTrigger: true);
+ })
+ .Build();
+app.Run();
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/README.md b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/README.md
new file mode 100644
index 0000000000..a5411bf375
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/README.md
@@ -0,0 +1,81 @@
+# Workflow as MCP Tool Sample
+
+This sample demonstrates how to expose durable workflows as [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) tools, enabling MCP-compatible clients to invoke workflows directly.
+
+## Key Concepts Demonstrated
+
+- **Workflow as MCP Tool**: Expose workflows as callable MCP tools using `exposeMcpToolTrigger: true`
+- **MCP Server Hosting**: The Azure Functions host automatically generates a remote MCP endpoint at `/runtime/webhooks/mcp`
+- **String and POCO Results**: Shows workflows returning both plain strings and structured JSON objects
+
+## Sample Architecture
+
+The sample creates two workflows exposed as MCP tools:
+
+### Translate Workflow (returns a string)
+
+| Executor | Input | Output | Description |
+|----------|-------|--------|-------------|
+| **TranslateText** | `string` | `TranslationResult` | Converts input text to uppercase |
+| **FormatOutput** | `TranslationResult` | `string` | Formats the result into a readable string |
+
+### OrderLookup Workflow (returns a POCO)
+
+| Executor | Input | Output | Description |
+|----------|-------|--------|-------------|
+| **LookupOrder** | `string` | `OrderInfo` | Looks up an order by ID |
+| **EnrichOrder** | `OrderInfo` | `OrderSummary` | Adds computed fields (total price, status) |
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for complete setup instructions, including:
+
+- Prerequisites installation
+- Durable Task Scheduler setup
+- Storage emulator configuration
+
+For this sample, you'll also need [Node.js](https://nodejs.org/en/download) to use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).
+
+## Running the Sample
+
+1. **Start the Function App**:
+
+ ```bash
+ cd dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool
+ func start
+ ```
+
+2. **Note the MCP Server Endpoint**: When the app starts, you'll see the MCP server endpoint in the terminal output:
+
+ ```text
+ MCP server endpoint: http://localhost:7071/runtime/webhooks/mcp
+ ```
+
+## Invoking Workflows via MCP Inspector
+
+1. Install and run the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
+
+ ```bash
+ npx @modelcontextprotocol/inspector
+ ```
+
+2. Connect to the MCP server endpoint:
+ - For **Transport Type**, select **"Streamable HTTP"**
+ - For **URL**, enter `http://localhost:7071/runtime/webhooks/mcp`
+ - Click the **Connect** button
+
+3. Click the **List Tools** button. You should see two tools: `Translate` and `OrderLookup`.
+
+4. Test the **Translate** tool (returns a plain string):
+ - Select the `Translate` tool
+ - Set `hello world` as the `input` parameter
+ - Click **Run Tool**
+ - Expected result: `Original: hello world => Translated: HELLO WORLD`
+
+5. Test the **OrderLookup** tool (returns a JSON object):
+ - 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`, and `Status`
+
+You'll see the workflow executor activities logged in the terminal where you ran `func start`.
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/host.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/host.json
new file mode 100644
index 0000000000..9384a0a583
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/host.json
@@ -0,0 +1,20 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "Microsoft.Agents.AI.DurableTask": "Information",
+ "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
+ "DurableTask": "Information",
+ "Microsoft.DurableTask": "Information"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "AzureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/local.settings.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/local.settings.json
new file mode 100644
index 0000000000..fcb6658e92
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/local.settings.json
@@ -0,0 +1,8 @@
+{
+ "IsEncrypted": false,
+ "Values": {
+ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
+ "AzureWebJobsStorage": "UseDevelopmentStorage=true",
+ "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj
new file mode 100644
index 0000000000..517dd323a7
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj
@@ -0,0 +1,42 @@
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+ WorkflowAndAgents
+ WorkflowAndAgents
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Executors.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Executors.cs
new file mode 100644
index 0000000000..727379b482
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Executors.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowAndAgents;
+
+internal sealed class TranslateText() : Executor("TranslateText")
+{
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($"[Activity] TranslateText: '{message}'");
+ return ValueTask.FromResult(new TranslationResult(message, message.ToUpperInvariant()));
+ }
+}
+
+internal sealed class FormatOutput() : Executor("FormatOutput")
+{
+ public override ValueTask HandleAsync(
+ TranslationResult message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine("[Activity] FormatOutput: Formatting result");
+ return ValueTask.FromResult($"Original: {message.Original} => Translated: {message.Translated}");
+ }
+}
+
+internal sealed record TranslationResult(string Original, string Translated);
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Program.cs
new file mode 100644
index 0000000000..51b9fb4d7f
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/Program.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates using ConfigureDurableOptions to register BOTH agents AND workflows
+// in a single Azure Functions app. It uses a workflow to translate text and a standalone AI agent
+// accessible via HTTP and MCP tool triggers.
+
+#pragma warning disable IDE0002 // Simplify Member Access
+
+using Azure;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+using OpenAI.Chat;
+using WorkflowAndAgents;
+
+// Get the Azure OpenAI endpoint and deployment name from environment variables.
+string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
+
+// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
+string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
+AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
+ ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ : new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
+
+ChatClient chatClient = client.GetChatClient(deploymentName);
+
+// Define a standalone AI agent
+AIAgent assistant = chatClient.AsAIAgent(
+ "You are a helpful assistant. Answer questions clearly and concisely.",
+ "Assistant",
+ description: "A general-purpose helpful assistant.");
+
+// Define workflow executors
+TranslateText translateText = new();
+FormatOutput formatOutput = new();
+
+// Build a workflow: TranslateText -> FormatOutput
+Workflow translateWorkflow = new WorkflowBuilder(translateText)
+ .WithName("Translate")
+ .WithDescription("Translate text to uppercase and format the result")
+ .AddEdge(translateText, formatOutput)
+ .Build();
+
+// Use ConfigureDurableOptions to register both agents and workflows together
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableOptions(options =>
+ {
+ // Register the standalone agent with HTTP and MCP tool triggers
+ options.Agents.AddAIAgent(assistant, enableHttpTrigger: true, enableMcpToolTrigger: true);
+
+ // Register the workflow with an HTTP endpoint and MCP tool trigger
+ options.Workflows.AddWorkflow(translateWorkflow, exposeStatusEndpoint: false, exposeMcpToolTrigger: true);
+ })
+ .Build();
+app.Run();
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/README.md b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/README.md
new file mode 100644
index 0000000000..37841777cc
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/README.md
@@ -0,0 +1,76 @@
+# Workflow and Agents Sample
+
+This sample demonstrates how to use `ConfigureDurableOptions` to register **both** AI agents **and** workflows in a single Azure Functions app. This is the recommended approach when your application needs both standalone agents and orchestrated workflows.
+
+## Key Concepts Demonstrated
+
+- **Unified Configuration**: Use `ConfigureDurableOptions` to register agents and workflows together
+- **Standalone Agent**: An AI agent accessible via HTTP and MCP tool triggers
+- **Workflow**: A simple text translation workflow also exposed as an MCP tool
+- **Mixed Triggers**: Both agents and workflows coexist in the same Functions host
+
+## Sample Architecture
+
+### Standalone Agent
+
+| Agent | Description |
+|-------|-------------|
+| **Assistant** | A general-purpose AI assistant accessible via HTTP (`/agents/Assistant/run`) and as an MCP tool |
+
+### Translate Workflow
+
+| Executor | Input | Output | Description |
+|----------|-------|--------|-------------|
+| **TranslateText** | `string` | `TranslationResult` | Converts input text to uppercase |
+| **FormatOutput** | `TranslationResult` | `string` | Formats the result into a readable string |
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for complete setup instructions, including:
+
+- Prerequisites installation
+- Durable Task Scheduler setup
+- Storage emulator configuration
+
+This sample also requires Azure OpenAI credentials. Set the following in `local.settings.json`:
+
+- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint URL
+- `AZURE_OPENAI_DEPLOYMENT_NAME`: Your chat model deployment name
+- `AZURE_OPENAI_API_KEY` (optional): If not set, Azure CLI credential is used
+
+## Running the Sample
+
+1. **Start the Function App**:
+
+ ```bash
+ cd dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents
+ func start
+ ```
+
+2. **Expected Functions**: When the app starts, you should see functions for both the agent and the workflow:
+
+ - `dafx-Assistant` (entity trigger for the agent)
+ - `http-Assistant` (HTTP trigger for the agent)
+ - `mcptool-Assistant` (MCP tool trigger for the agent)
+ - `wf-Translate` (orchestration trigger for the workflow)
+ - `mcptool-wf-Translate` (MCP tool trigger for the workflow)
+
+## Invoking the Agent via HTTP
+
+```bash
+curl -X POST http://localhost:7071/agents/Assistant/run \
+ -H "Content-Type: application/json" \
+ -d '{"query": "What is the capital of France?"}'
+```
+
+## Invoking via MCP Inspector
+
+1. Install and run the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
+
+ ```bash
+ npx @modelcontextprotocol/inspector
+ ```
+
+2. Connect to `http://localhost:7071/runtime/webhooks/mcp` using **Streamable HTTP** transport.
+
+3. Click **List Tools** to see both the `Assistant` agent tool and the `Translate` workflow tool.
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/host.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/host.json
new file mode 100644
index 0000000000..9384a0a583
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/host.json
@@ -0,0 +1,20 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "Microsoft.Agents.AI.DurableTask": "Information",
+ "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
+ "DurableTask": "Information",
+ "Microsoft.DurableTask": "Information"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "AzureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/local.settings.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/local.settings.json
new file mode 100644
index 0000000000..5f6d7d3340
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/local.settings.json
@@ -0,0 +1,10 @@
+{
+ "IsEncrypted": false,
+ "Values": {
+ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
+ "AzureWebJobsStorage": "UseDevelopmentStorage=true",
+ "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
+ "AZURE_OPENAI_ENDPOINT": "",
+ "AZURE_OPENAI_DEPLOYMENT_NAME": ""
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/README.md b/dotnet/samples/04-hosting/DurableWorkflows/README.md
index 2b7103de50..f2386380d7 100644
--- a/dotnet/samples/04-hosting/DurableWorkflows/README.md
+++ b/dotnet/samples/04-hosting/DurableWorkflows/README.md
@@ -48,3 +48,4 @@ $env:DURABLE_TASK_SCHEDULER_CONNECTION_STRING = "AccountEndpoint=http://localhos
| [01_SequentialWorkflow](AzureFunctions/01_SequentialWorkflow/) | Sequential workflow hosted in Azure Functions |
| [02_ConcurrentWorkflow](AzureFunctions/02_ConcurrentWorkflow/) | Concurrent workflow hosted in Azure Functions |
| [03_WorkflowHITL](AzureFunctions/03_WorkflowHITL/) | Human-in-the-loop workflow hosted in Azure Functions |
+| [04_WorkflowMcpTool](AzureFunctions/04_WorkflowMcpTool/) | Workflow exposed as an MCP tool |
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
index 8239ff17cc..64ec846eb8 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
@@ -167,6 +167,20 @@ public async ValueTask ExecuteAsync(FunctionContext context)
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}.");
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
index 6dc1ab2244..e6c94347a1 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
@@ -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,55 @@ await agentProxy.RunAsync(
return agentResponse.Text;
}
+ ///
+ /// Runs a workflow via MCP tool trigger.
+ /// Extracts the input argument, schedules a new orchestration, waits for completion, and returns the output.
+ ///
+ public static async Task 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 orchestrationInput = new() { Input = input };
+ string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, orchestrationInput);
+
+ OrchestrationMetadata? metadata = await client.WaitForInstanceCompletionAsync(
+ instanceId,
+ getInputsAndOutputs: true,
+ cancellation: functionContext.CancellationToken);
+
+ if (metadata is null)
+ {
+ throw new InvalidOperationException($"Workflow orchestration '{instanceId}' returned no metadata.");
+ }
+
+ if (metadata.RuntimeStatus is OrchestrationRuntimeStatus.Failed)
+ {
+ string errorMessage = metadata.FailureDetails?.ErrorMessage ?? "Unknown error";
+ throw new InvalidOperationException($"Workflow orchestration '{instanceId}' failed: {errorMessage}");
+ }
+
+ if (metadata.RuntimeStatus is not OrchestrationRuntimeStatus.Completed)
+ {
+ throw new InvalidOperationException($"Workflow orchestration '{instanceId}' ended with unexpected status '{metadata.RuntimeStatus}'.");
+ }
+
+ return metadata.ReadOutputAs()?.Result;
+ }
+
///
/// Creates an error response with the specified status code and error message.
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md
index 93c90bba9c..2c188757d5 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md
@@ -2,6 +2,7 @@
## [Unreleased]
+- Added MCP tool trigger support for durable workflows ([#4768](https://github.com/microsoft/agent-framework/pull/4768))
- Added Azure Functions hosting support for durable workflows ([#4436](https://github.com/microsoft/agent-framework/pull/4436))
## v1.0.0-preview.251219.1
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionMetadataFactory.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionMetadataFactory.cs
index d88cd939d9..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;
@@ -98,4 +99,65 @@ internal static DefaultFunctionMetadata CreateOrchestrationTrigger(string functi
ScriptFile = BuiltInFunctions.ScriptFile,
};
}
+
+ ///
+ /// Creates function metadata for an MCP tool trigger function that starts a workflow.
+ ///
+ /// The name of the workflow to expose as an MCP tool.
+ /// An optional description for the MCP tool. If null, a default description is generated.
+ /// A configured for an MCP tool trigger.
+ internal static DefaultFunctionMetadata CreateWorkflowMcpToolTrigger(
+ string workflowName,
+ string? description)
+ {
+ var functionName = $"{BuiltInFunctions.McpToolPrefix}{workflowName}";
+ var toolDescription = description ?? $"Run the {workflowName} workflow";
+
+ var toolProperties = new JsonArray(new JsonObject
+ {
+ ["propertyName"] = "input",
+ ["propertyType"] = "string",
+ ["description"] = "The input to the workflow.",
+ ["isRequired"] = true,
+ ["isArray"] = false,
+ });
+
+ var triggerBinding = new JsonObject
+ {
+ ["name"] = "context",
+ ["type"] = "mcpToolTrigger",
+ ["direction"] = "In",
+ ["toolName"] = workflowName,
+ ["description"] = toolDescription,
+ ["toolProperties"] = toolProperties.ToJsonString(),
+ };
+
+ var inputBinding = new JsonObject
+ {
+ ["name"] = "input",
+ ["type"] = "mcpToolProperty",
+ ["direction"] = "In",
+ ["propertyName"] = "input",
+ ["description"] = "The input to the workflow",
+ ["isRequired"] = true,
+ ["dataType"] = "String",
+ ["propertyType"] = "string",
+ };
+
+ var clientBinding = new JsonObject
+ {
+ ["name"] = "client",
+ ["type"] = "durableClient",
+ ["direction"] = "In",
+ };
+
+ return new DefaultFunctionMetadata
+ {
+ Name = functionName,
+ Language = "dotnet-isolated",
+ RawBindings = [triggerBinding.ToJsonString(), inputBinding.ToJsonString(), clientBinding.ToJsonString()],
+ EntryPoint = BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint,
+ ScriptFile = BuiltInFunctions.ScriptFile,
+ };
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
index ceb47c389a..959ffab2f6 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
@@ -67,6 +67,13 @@ public static FunctionsApplicationBuilder ConfigureDurableOptions(
builder.Services.ConfigureDurableOptions(configure);
+ if (sharedOptions.Agents.GetAgentFactories().Count > 0)
+ {
+ builder.Services.TryAddSingleton(_ =>
+ new DefaultFunctionsAgentOptionsProvider(DurableAgentsOptionsExtensions.GetAgentOptionsSnapshot()));
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ }
+
if (sharedOptions.Workflows.Workflows.Count > 0)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
@@ -102,12 +109,14 @@ private static void EnsureMiddlewareRegistered(FunctionsApplicationBuilder build
builder.UseWhen(static context =>
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrchestrationHttpFunctionEntryPoint, StringComparison.Ordinal) ||
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();
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsDurableOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsDurableOptions.cs
index 6e7b6ec5a8..ee9051fa0d 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsDurableOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsDurableOptions.cs
@@ -10,6 +10,7 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
internal sealed class FunctionsDurableOptions : DurableOptions
{
private readonly HashSet _statusEndpointWorkflows = new(StringComparer.OrdinalIgnoreCase);
+ private readonly HashSet _mcpToolTriggerWorkflows = new(StringComparer.OrdinalIgnoreCase);
///
/// Enables the status HTTP endpoint for the specified workflow.
@@ -26,4 +27,20 @@ internal bool IsStatusEndpointEnabled(string workflowName)
{
return this._statusEndpointWorkflows.Contains(workflowName);
}
+
+ ///
+ /// Enables the MCP tool trigger for the specified workflow.
+ ///
+ internal void EnableMcpToolTrigger(string workflowName)
+ {
+ this._mcpToolTriggerWorkflows.Add(workflowName);
+ }
+
+ ///
+ /// Returns whether the MCP tool trigger is enabled for the specified workflow.
+ ///
+ internal bool IsMcpToolTriggerEnabled(string workflowName)
+ {
+ return this._mcpToolTriggerWorkflows.Contains(workflowName);
+ }
}
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 6f40cbb791..7cf38397ae 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowOptionsExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowOptionsExtensions.cs
@@ -27,4 +27,31 @@ public static void AddWorkflow(this DurableWorkflowOptions options, Workflow wor
functionsOptions.EnableStatusEndpoint(workflow.Name!);
}
}
+
+ ///
+ /// 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.
+ /// If , a GET endpoint is generated at workflows/{name}/status/{runId}.
+ /// If , an MCP tool trigger is generated for the workflow.
+ 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!);
+ }
+ }
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowsFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowsFunctionMetadataTransformer.cs
index c7ad9a5ebd..8066eefccc 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowsFunctionMetadataTransformer.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowsFunctionMetadataTransformer.cs
@@ -113,6 +113,17 @@ public void Transform(IList original)
}
}
+ // 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 entry in workflow.Value.ReflectExecutors())
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 efb02b1aff..af62436ea4 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs
@@ -5,6 +5,8 @@
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests;
///
@@ -235,6 +237,114 @@ await this.WaitForConditionAsync(
});
}
+ [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 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 { { "input", "hello world" } });
+
+ 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);
+
+ // 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 { { "input", "ORD-2025-42" } });
+
+ 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);
+
+ // 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 WorkflowAndAgentsSampleValidationAsync()
+ {
+ string samplePath = Path.Combine(s_samplesPath, "05_WorkflowAndAgents");
+ await this.RunSampleTestAsync(samplePath, requiresOpenAI: true, 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 the agent and workflow tools are listed
+ IList tools = await mcpClient.ListToolsAsync();
+ this._outputHelper.WriteLine($"MCP tools found: {string.Join(", ", tools.Select(t => t.Name))}");
+
+ Assert.Single(tools, t => t.Name == "Assistant");
+ Assert.Single(tools, t => t.Name == "Translate");
+
+ // Invoke the Translate workflow via MCP tool
+ this._outputHelper.WriteLine("Invoking MCP tool 'Translate'...");
+ CallToolResult translateResult = await mcpClient.CallToolAsync(
+ "Translate",
+ arguments: new Dictionary { { "input", "hello world" } });
+
+ Assert.NotEmpty(translateResult.Content);
+ string translateResponse = Assert.IsType(translateResult.Content[0]).Text;
+ this._outputHelper.WriteLine($"Translate MCP tool response: {translateResponse}");
+ Assert.Contains("HELLO WORLD", translateResponse);
+
+ // Invoke the Assistant agent via MCP tool
+ this._outputHelper.WriteLine("Invoking MCP tool 'Assistant'...");
+ CallToolResult assistantResult = await mcpClient.CallToolAsync(
+ "Assistant",
+ arguments: new Dictionary { { "query", "What is 2 + 2?" } });
+
+ Assert.NotEmpty(assistantResult.Content);
+ string assistantResponse = Assert.IsType(assistantResult.Content[0]).Text;
+ this._outputHelper.WriteLine($"Assistant MCP tool response: {assistantResponse}");
+ Assert.NotEmpty(assistantResponse);
+
+ // Verify workflow 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.");
+ }
+ });
+ }
+
[Fact]
public async Task ConcurrentWorkflowSampleValidationAsync()
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/FunctionMetadataFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/FunctionMetadataFactoryTests.cs
new file mode 100644
index 0000000000..a777c69480
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/FunctionMetadataFactoryTests.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+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);
+
+ // 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]);
+ 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]);
+ }
+}