Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
427 changes: 427 additions & 0 deletions docs/decisions/0047-dotnet-workflow-owned-mcp-sessions.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.12" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.12" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.12" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.12" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.10.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.12" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.12" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
// through the reserved tools/list operation, then calls microsoft_docs_search from the workflow.

using System.ClientModel;
using System.Collections.Concurrent;
using System.Net.Http.Headers;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI.Workflows.Declarative.Mcp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenAI.Responses;
using Shared.Foundry;
using Shared.Workflows;
Expand All @@ -30,6 +30,7 @@ namespace Demo.Workflows.Declarative.InvokeFoundryToolboxMcp;
/// </remarks>
internal sealed class Program
{
private const string McpHttpClientName = "foundry-toolbox-mcp";
private const string ToolboxNameSetting = "FOUNDRY_TOOLBOX_NAME";
private const string ToolboxApiVersionSetting = "FOUNDRY_AGENT_TOOLSET_API_VERSION";
private const string ToolboxMcpServerUrlSetting = "FOUNDRY_TOOLBOX_MCP_SERVER_URL";
Expand Down Expand Up @@ -73,51 +74,32 @@ public static async Task Main(string[] args)
// Get input from command line or console
string workflowInput = Application.GetInput(args);

// Create the MCP tool handler for invoking the Foundry toolbox MCP proxy.
ConcurrentBag<HttpClient> createdHttpClients = [];
DefaultMcpToolHandler mcpToolHandler = new(
httpClientProvider: async (serverUrl, _) =>
// Reuse HTTP handlers while each MCP invocation owns its client.
ServiceCollection services = new();
services.AddHttpClient(McpHttpClientName)
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
await Task.CompletedTask.ConfigureAwait(false);

if (!string.Equals(serverUrl, toolboxMcpServerUrl, StringComparison.OrdinalIgnoreCase))
{
return null;
}

FoundryToolboxBearerTokenHandler handler = new(credential)
{
InnerHandler = new HttpClientHandler()
};
HttpClient httpClient = new(handler);
createdHttpClients.Add(httpClient);
return httpClient;
});

try
{
// Create the workflow factory with MCP tool provider
WorkflowFactory workflowFactory = new("InvokeFoundryToolboxMcp.yaml", foundryEndpoint)
{
Configuration = workflowConfiguration,
AllowedEnvironmentVariables = [ToolboxMcpServerUrlSetting, DocsServerLabelSetting, WebSearchToolNameSetting],
McpToolHandler = mcpToolHandler
};

// Execute the workflow
WorkflowRunner runner = new() { UseJsonCheckpoints = true };
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
}
finally
UseCookies = false,
AllowAutoRedirect = false,
CheckCertificateRevocationList = true
})
.AddHttpMessageHandler(() => new FoundryToolboxBearerTokenHandler(credential, new Uri(toolboxMcpServerUrl)))
.SetHandlerLifetime(TimeSpan.FromMinutes(2));
await using ServiceProvider serviceProvider = services.BuildServiceProvider();
await using DefaultMcpToolHandler mcpToolHandler = new(
serviceProvider.GetRequiredService<IHttpClientFactory>(), McpHttpClientName);

// Create the workflow factory with MCP tool provider
WorkflowFactory workflowFactory = new("InvokeFoundryToolboxMcp.yaml", foundryEndpoint)
{
// Clean up connections and dispose created HttpClients
await mcpToolHandler.DisposeAsync();
Configuration = workflowConfiguration,
AllowedEnvironmentVariables = [ToolboxMcpServerUrlSetting, DocsServerLabelSetting, WebSearchToolNameSetting],
McpToolHandler = mcpToolHandler
};

foreach (HttpClient httpClient in createdHttpClients)
{
httpClient.Dispose();
}
}
// Execute the workflow
WorkflowRunner runner = new() { UseJsonCheckpoints = true };
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
}

private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration, TokenCredential credential)
Expand Down Expand Up @@ -180,17 +162,25 @@ private static async Task<string> CreateSampleToolboxAsync(string name, string s
private static string BuildToolboxMcpServerUrl(string toolboxEndpoint, string toolboxName, string apiVersion) =>
$"{toolboxEndpoint.TrimEnd('/')}/{toolboxName}/mcp?api-version={Uri.EscapeDataString(apiVersion)}";

private sealed class FoundryToolboxBearerTokenHandler(TokenCredential credential) : DelegatingHandler
private sealed class FoundryToolboxBearerTokenHandler(TokenCredential credential, Uri serverEndpoint) : DelegatingHandler
{
private static readonly TokenRequestContext s_tokenContext =
new(["https://ai.azure.com/.default"]);

private readonly string _serverOrigin = serverEndpoint.GetLeftPart(UriPartial.Authority);

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
AccessToken token = await credential.GetTokenAsync(s_tokenContext, cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
Uri? requestUri = request.RequestUri;
if (requestUri is not null
&& string.Equals(requestUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
&& string.Equals(requestUri.GetLeftPart(UriPartial.Authority), this._serverOrigin, StringComparison.OrdinalIgnoreCase))
{
AccessToken token = await credential.GetTokenAsync(s_tokenContext, cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
}

return await base.SendAsync(request, cancellationToken);
}
Expand Down
101 changes: 47 additions & 54 deletions dotnet/samples/03-workflows/Declarative/InvokeMcpTool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
// server tools directly from a declarative workflow. MCP servers expose tools that can be
// invoked to perform specific tasks, like searching documentation or executing operations.

using System.Net.Http.Headers;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI.Workflows.Declarative.Mcp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Shared.Foundry;
using Shared.Workflows;

Expand Down Expand Up @@ -41,6 +43,8 @@ namespace Demo.Workflows.Declarative.InvokeMcpTool;
/// </remarks>
internal sealed class Program
{
private const string McpHttpClientName = "workflow-mcp";

public static async Task Main(string[] args)
{
// Initialize configuration
Expand All @@ -53,65 +57,33 @@ public static async Task Main(string[] args)
// Get input from command line or console
string workflowInput = Application.GetInput(args);

// Create the MCP tool handler for invoking MCP server tools.
// The HttpClient callback allows configuring authentication per MCP server.
// Different MCP servers may require different authentication configurations.
// For Production scenarios, consider implementing a more robust HttpClient management strategy to reuse HttpClient instances and manage their lifetimes appropriately.
List<HttpClient> createdHttpClients = [];
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
DefaultAzureCredential credential = new();
DefaultMcpToolHandler mcpToolHandler = new(
httpClientProvider: async (serverUrl, cancellationToken) =>
{
if (serverUrl.StartsWith("https://mcp.ai.azure.com", StringComparison.OrdinalIgnoreCase))
{
// Acquire token for the Azure MCP server
AccessToken token = await credential.GetTokenAsync(
new TokenRequestContext(["https://mcp.ai.azure.com/.default"]),
cancellationToken);

// Create HttpClient with Authorization header
HttpClient httpClient = new();
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
createdHttpClients.Add(httpClient);
return httpClient;
}

if (serverUrl.StartsWith("https://learn.microsoft.com", StringComparison.OrdinalIgnoreCase))
{
// Microsoft Learn MCP server does not require authentication
HttpClient httpClient = new();
createdHttpClients.Add(httpClient);
return httpClient;
}

// Return null for unknown servers to use the default HttpClient without auth.
return null;
});

try
{
// Create the workflow factory with MCP tool provider
WorkflowFactory workflowFactory = new("InvokeMcpTool.yaml", foundryEndpoint)
{
McpToolHandler = mcpToolHandler
};

// Execute the workflow
WorkflowRunner runner = new() { UseJsonCheckpoints = true };
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
}
finally
// Reuse HTTP handlers while each MCP invocation owns its client.
ServiceCollection services = new();
services.AddHttpClient(McpHttpClientName)
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
UseCookies = false,
AllowAutoRedirect = false,
CheckCertificateRevocationList = true
})
.AddHttpMessageHandler(() => new AzureMcpBearerTokenHandler(credential))
.SetHandlerLifetime(TimeSpan.FromMinutes(2));
await using ServiceProvider serviceProvider = services.BuildServiceProvider();
await using DefaultMcpToolHandler mcpToolHandler = new(
serviceProvider.GetRequiredService<IHttpClientFactory>(), McpHttpClientName);

// Create the workflow factory with MCP tool provider
WorkflowFactory workflowFactory = new("InvokeMcpTool.yaml", foundryEndpoint)
{
// Clean up connections and dispose created HttpClients
await mcpToolHandler.DisposeAsync();
McpToolHandler = mcpToolHandler
};

foreach (HttpClient httpClient in createdHttpClients)
{
httpClient.Dispose();
}
}
// Execute the workflow
WorkflowRunner runner = new() { UseJsonCheckpoints = true };
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
}

private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration)
Expand All @@ -138,4 +110,25 @@ Be concise and helpful in your responses.
"""
};
}

private sealed class AzureMcpBearerTokenHandler(TokenCredential credential) : DelegatingHandler
{
private static readonly TokenRequestContext s_tokenContext =
new(["https://mcp.ai.azure.com/.default"]);

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
Uri? requestUri = request.RequestUri;
if (requestUri is not null
&& string.Equals(requestUri.GetLeftPart(UriPartial.Authority), "https://mcp.ai.azure.com", StringComparison.OrdinalIgnoreCase))
{
AccessToken token = await credential.GetTokenAsync(s_tokenContext, cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
}

return await base.SendAsync(request, cancellationToken);
}
}
}
Loading
Loading