diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 195521b1eb2..93cd6ade5ad 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -239,6 +239,7 @@
+
diff --git a/dotnet/eng/verify-samples/WorkflowSamples.cs b/dotnet/eng/verify-samples/WorkflowSamples.cs
index 2842f4af894..2793dd04c53 100644
--- a/dotnet/eng/verify-samples/WorkflowSamples.cs
+++ b/dotnet/eng/verify-samples/WorkflowSamples.cs
@@ -478,6 +478,17 @@ internal static class WorkflowSamples
ExpectedOutputDescription = ["The output should show a workflow invoking a function tool (e.g. a menu plugin) to answer a question about the soup of the day."],
},
+ new SampleDefinition
+ {
+ Name = "Workflow_Declarative_InvokeFoundryToolboxMcp",
+ ProjectPath = "samples/03-workflows/Declarative/InvokeFoundryToolboxMcp",
+ RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
+ OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME", "FOUNDRY_TOOLBOX_NAME", "FOUNDRY_AGENT_TOOLSET_API_VERSION"],
+ Inputs = ["How do I use Azure OpenAI with my data?"],
+ InputDelayMs = 3000,
+ ExpectedOutputDescription = ["The output should show a workflow using Foundry Toolbox MCP tools to search Microsoft Learn documentation and web search to provide a summary of results."],
+ },
+
new SampleDefinition
{
Name = "Workflow_Declarative_InvokeMcpTool",
diff --git a/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.csproj b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.csproj
new file mode 100644
index 00000000000..3e70c3f9944
--- /dev/null
+++ b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.csproj
@@ -0,0 +1,42 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+ true
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.yaml b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.yaml
new file mode 100644
index 00000000000..b5f6f39316e
--- /dev/null
+++ b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.yaml
@@ -0,0 +1,87 @@
+#
+# This workflow demonstrates invoking MCP tools through a Foundry toolbox MCP proxy.
+#
+# The toolbox is provisioned with TWO different tool types:
+# 1. A Foundry built-in web_search tool
+# 2. A Microsoft Learn MCP server (microsoft_docs)
+# Both are surfaced through the same MCP-compatible toolbox endpoint.
+#
+# The workflow:
+# 1. Accepts a documentation/web search query as input
+# 2. Lists the tools exposed by the Foundry toolbox using reserved toolName: tools/list
+# 3. Invokes the microsoft_docs_search MCP tool
+# 4. Invokes the built-in web_search tool against the same toolbox endpoint
+# 5. Uses an agent to summarize and combine both result sets
+#
+# Example input:
+# How do I use Azure OpenAI with my data?
+#
+kind: Workflow
+trigger:
+
+ kind: OnConversationStart
+ id: workflow_invoke_foundry_toolbox_mcp
+ actions:
+
+ # Set the search query from user input.
+ - kind: SetVariable
+ id: set_search_query
+ variable: Local.SearchQuery
+ value: =System.LastMessage.Text
+
+ # List tools exposed by the Foundry toolbox MCP proxy.
+ - kind: InvokeMcpTool
+ id: list_toolbox_tools
+ serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL
+ serverLabel: foundry_toolbox
+ toolName: tools/list
+ conversationId: =System.ConversationId
+ headers:
+ Foundry-Features: Toolboxes=V1Preview
+ output:
+ autoSend: true
+ result: Local.ToolboxTools
+
+ # Invoke a specific tool exposed through the toolbox and add the result to the conversation.
+ - kind: InvokeMcpTool
+ id: search_docs_with_toolbox
+ serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL
+ serverLabel: foundry_toolbox
+ toolName: =Env.FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL & "___microsoft_docs_search"
+ conversationId: =System.ConversationId
+ headers:
+ Foundry-Features: Toolboxes=V1Preview
+ arguments:
+ query: =Local.SearchQuery
+ output:
+ autoSend: true
+ result: Local.SearchResult
+
+ # Invoke the web_search built-in tool through the same toolbox proxy. The toolbox surfaces
+ # built-in Foundry tools (like web_search) alongside MCP tools through one MCP-compatible
+ # endpoint. Note that web_search expects argument 'search_query' (not 'query').
+ - kind: InvokeMcpTool
+ id: search_web_with_toolbox
+ serverUrl: =Env.FOUNDRY_TOOLBOX_MCP_SERVER_URL
+ serverLabel: foundry_toolbox
+ toolName: =Env.FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME
+ conversationId: =System.ConversationId
+ headers:
+ Foundry-Features: Toolboxes=V1Preview
+ arguments:
+ search_query: =Local.SearchQuery
+ output:
+ autoSend: true
+ result: Local.WebSearchResult
+
+ # Use the agent to summarize what happened and answer from the toolbox result.
+ - kind: InvokeAzureAgent
+ id: summarize_toolbox_result
+ agent:
+ name: FoundryToolboxMcpAgent
+ conversationId: =System.ConversationId
+ input:
+ messages: =UserMessage("Combine the Microsoft Learn docs results and the Foundry web search results in the conversation to answer the query " & Local.SearchQuery)
+ output:
+ autoSend: true
+ messages: Local.Summary
diff --git a/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs
new file mode 100644
index 00000000000..6636cb13a7f
--- /dev/null
+++ b/dotnet/samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/Program.cs
@@ -0,0 +1,218 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates using InvokeMcpTool to call MCP tools through a Foundry toolbox.
+// It creates a sample toolbox that exposes Microsoft Learn MCP tools, lists the toolbox tools
+// through the reserved tools/list operation, then calls microsoft_docs_search from the workflow.
+
+using System.ClientModel;
+using System.ClientModel.Primitives;
+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 OpenAI.Responses;
+using Shared.Foundry;
+using Shared.Workflows;
+
+#pragma warning disable OPENAI001 // Experimental API
+#pragma warning disable AAIP001 // AgentToolboxes is experimental
+
+namespace Demo.Workflows.Declarative.InvokeFoundryToolboxMcp;
+
+///
+/// Demonstrates a workflow that uses InvokeMcpTool to call MCP tools exposed through a Foundry toolbox.
+///
+///
+/// This sample provisions a toolbox with Microsoft Learn MCP tools, uses the reserved
+/// tools/list tool name to list the toolbox tools, calls one specific toolbox tool,
+/// and has a Foundry agent summarize the results.
+///
+internal sealed class Program
+{
+ 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";
+ private const string DocsServerLabelSetting = "FOUNDRY_TOOLBOX_DOCS_SERVER_LABEL";
+ private const string WebSearchToolNameSetting = "FOUNDRY_TOOLBOX_WEB_SEARCH_TOOL_NAME";
+ private const string DefaultToolboxName = "declarative_foundry_toolbox_mcp";
+ private const string DefaultToolboxApiVersion = "v1";
+ private const string DefaultDocsServerLabel = "microsoft_docs";
+ private const string DefaultWebSearchToolName = "web_search";
+
+ public static async Task Main(string[] args)
+ {
+ // Initialize configuration
+ IConfiguration configuration = Application.InitializeConfig();
+ Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
+ string toolboxName = configuration[ToolboxNameSetting] ?? DefaultToolboxName;
+ string toolboxApiVersion = configuration[ToolboxApiVersionSetting] ?? DefaultToolboxApiVersion;
+ string docsServerLabel = configuration[DocsServerLabelSetting] ?? DefaultDocsServerLabel;
+ string webSearchToolName = configuration[WebSearchToolNameSetting] ?? DefaultWebSearchToolName;
+
+ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+ // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+ DefaultAzureCredential credential = new();
+
+ // Ensure sample toolbox and agent exist in Foundry
+ string toolboxEndpoint = await CreateSampleToolboxAsync(toolboxName, docsServerLabel, foundryEndpoint, credential);
+ string toolboxMcpServerUrl = BuildToolboxMcpServerUrl(toolboxEndpoint, toolboxName, toolboxApiVersion);
+ IConfiguration workflowConfiguration = new ConfigurationBuilder()
+ .AddConfiguration(configuration)
+ .AddInMemoryCollection(new Dictionary
+ {
+ [ToolboxMcpServerUrlSetting] = toolboxMcpServerUrl,
+ [DocsServerLabelSetting] = docsServerLabel,
+ [WebSearchToolNameSetting] = webSearchToolName,
+ })
+ .Build();
+
+ await CreateAgentAsync(foundryEndpoint, configuration, credential);
+
+ // 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 createdHttpClients = [];
+ DefaultMcpToolHandler mcpToolHandler = new(
+ httpClientProvider: async (serverUrl, _) =>
+ {
+ 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,
+ McpToolHandler = mcpToolHandler
+ };
+
+ // Execute the workflow
+ WorkflowRunner runner = new() { UseJsonCheckpoints = true };
+ await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
+ }
+ finally
+ {
+ // Clean up connections and dispose created HttpClients
+ await mcpToolHandler.DisposeAsync();
+
+ foreach (HttpClient httpClient in createdHttpClients)
+ {
+ httpClient.Dispose();
+ }
+ }
+ }
+
+ private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration, TokenCredential credential)
+ {
+ AIProjectClient aiProjectClient = new(foundryEndpoint, credential);
+
+ await aiProjectClient.CreateAgentAsync(
+ agentName: "FoundryToolboxMcpAgent",
+ agentDefinition: DefineToolboxAgent(configuration),
+ agentDescription: "Summarizes Foundry toolbox MCP tool results");
+ }
+
+ private static DeclarativeAgentDefinition DefineToolboxAgent(IConfiguration configuration)
+ {
+ return new DeclarativeAgentDefinition(configuration.GetValue(Application.Settings.FoundryModel))
+ {
+ Instructions =
+ """
+ You are a helpful assistant that explains results produced by tools exposed through a Foundry toolbox.
+ The conversation history contains output from BOTH a Microsoft Learn documentation search (MCP) and a Foundry web search.
+ Synthesize an answer that draws on both sources, calls out where they agree or differ, and notes which toolbox tool produced each fact when it is relevant.
+ Be concise.
+ """
+ };
+ }
+
+ private static async Task CreateSampleToolboxAsync(string name, string serverLabel, Uri foundryEndpoint, TokenCredential credential)
+ {
+ AgentAdministrationClientOptions options = new();
+ options.AddPolicy(new FoundryFeaturesPolicy("Toolboxes=V1Preview"), PipelinePosition.PerCall);
+ AgentAdministrationClient adminClient = new(foundryEndpoint, credential, options);
+ AgentToolboxes toolboxClient = adminClient.GetAgentToolboxes();
+
+ try
+ {
+ await toolboxClient.DeleteToolboxAsync(name);
+ Console.WriteLine($"Deleted existing toolbox '{name}'");
+ }
+ catch (ClientResultException ex) when (ex.Status == 404)
+ {
+ // Toolbox does not exist.
+ }
+
+ ProjectsAgentTool webTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateWebSearchTool());
+
+ ProjectsAgentTool mcpTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateMcpTool(
+ serverLabel: serverLabel,
+ serverUri: new Uri("https://learn.microsoft.com/api/mcp"),
+ toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)));
+
+ ToolboxVersion created = (await toolboxClient.CreateToolboxVersionAsync(
+ name: name,
+ tools: [webTool, mcpTool],
+ description: "Sample toolbox combining Foundry web search with the Microsoft Learn MCP tools for the declarative InvokeFoundryToolboxMcp sample.")).Value;
+
+ Console.WriteLine($"Created toolbox '{created.Name}' v{created.Version} ({created.Tools.Count} tool(s))");
+
+ return $"{foundryEndpoint.ToString().TrimEnd('/')}/toolboxes";
+ }
+
+ 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 static readonly TokenRequestContext s_tokenContext =
+ new(["https://ai.azure.com/.default"]);
+
+ protected override async Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ AccessToken token = await credential.GetTokenAsync(s_tokenContext, cancellationToken);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
+
+ return await base.SendAsync(request, cancellationToken);
+ }
+ }
+
+ private sealed class FoundryFeaturesPolicy(string feature) : PipelinePolicy
+ {
+ private const string FeatureHeader = "Foundry-Features";
+
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ message.Request.Headers.Add(FeatureHeader, feature);
+ ProcessNext(message, pipeline, currentIndex);
+ }
+
+ public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ message.Request.Headers.Add(FeatureHeader, feature);
+ return ProcessNextAsync(message, pipeline, currentIndex);
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs
index 681cd5dc85f..66da428cf60 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs
@@ -3,12 +3,15 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
@@ -24,6 +27,14 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Mcp;
///
public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
{
+ ///
+ /// Reserved toolName value that maps an request
+ /// to the MCP protocol tools/list discovery operation.
+ ///
+ public const string ListToolsToolName = "tools/list";
+
+ private static readonly JsonWriterOptions s_toolListJsonWriterOptions = new() { Indented = true };
+
private readonly Func>? _httpClientProvider;
private readonly Dictionary _clients = [];
private readonly Dictionary _ownedHttpClients = [];
@@ -53,9 +64,18 @@ public async Task InvokeToolAsync(
CancellationToken cancellationToken = default)
{
// TODO: Handle connectionName and server label appropriately when Hosted scenario supports them. For now, ignore
- McpServerToolResultContent resultContent = new(Guid.NewGuid().ToString());
+ if (IsListToolsToolName(toolName))
+ {
+ ThrowIfListToolsArgumentsSpecified(arguments);
+ McpClient listToolsClient = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, cancellationToken).ConfigureAwait(false);
+ IList tools = await listToolsClient.ListToolsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ return CreateListToolsResultContent(tools.Select(tool => tool.ProtocolTool));
+ }
+
McpClient client = await this.GetOrCreateClientAsync(serverUrl, serverLabel, headers, cancellationToken).ConfigureAwait(false);
+ McpServerToolResultContent resultContent = new(Guid.NewGuid().ToString());
+
// Convert IDictionary to IReadOnlyDictionary for CallToolAsync
IReadOnlyDictionary? readOnlyArguments = arguments is null
? null
@@ -72,6 +92,23 @@ public async Task InvokeToolAsync(
return resultContent;
}
+ internal static bool IsListToolsToolName(string toolName) =>
+ string.Equals(toolName, ListToolsToolName, StringComparison.Ordinal);
+
+ internal static McpServerToolResultContent CreateListToolsResultContent(IEnumerable tools)
+ {
+ Throw.IfNull(tools);
+
+ McpServerToolResultContent resultContent = new(Guid.NewGuid().ToString())
+ {
+ Outputs = []
+ };
+
+ resultContent.Outputs.Add(new TextContent(SerializeToolsList(tools)));
+
+ return resultContent;
+ }
+
///
public async ValueTask DisposeAsync()
{
@@ -183,6 +220,16 @@ private static string ComputeHeadersHash(IDictionary? headers)
return hashCode.ToString(CultureInfo.InvariantCulture);
}
+ private static void ThrowIfListToolsArgumentsSpecified(IDictionary? arguments)
+ {
+ if (arguments is { Count: > 0 })
+ {
+ throw new ArgumentException(
+ $"The reserved MCP '{ListToolsToolName}' operation does not accept tool arguments.",
+ nameof(arguments));
+ }
+ }
+
private static void PopulateResultContent(McpServerToolResultContent resultContent, CallToolResult result)
{
// Ensure Outputs list is initialized
@@ -230,6 +277,17 @@ internal static AIContent ConvertContentBlock(ContentBlock block)
TextContentBlock text => new TextContent(text.Text),
ImageContentBlock image => CreateDataContent(image.Data, image.MimeType ?? "image/*"),
AudioContentBlock audio => CreateDataContent(audio.Data, audio.MimeType ?? "audio/*"),
+ EmbeddedResourceBlock embedded => ConvertEmbeddedResource(embedded),
+ _ => new TextContent(block.ToString() ?? string.Empty),
+ };
+ }
+
+ private static AIContent ConvertEmbeddedResource(EmbeddedResourceBlock block)
+ {
+ return block.Resource switch
+ {
+ TextResourceContents text => new TextContent(text.Text),
+ BlobResourceContents blob => CreateDataContent(blob.Blob, blob.MimeType ?? "application/octet-stream"),
_ => new TextContent(block.ToString() ?? string.Empty),
};
}
@@ -255,4 +313,39 @@ private static DataContent CreateDataContent(ReadOnlyMemory base64Utf8Data
return new DataContent($"data:{mediaType};base64,{base64}", mediaType);
}
+
+ private static string SerializeToolsList(IEnumerable tools)
+ {
+ using MemoryStream stream = new();
+ using (Utf8JsonWriter writer = new(stream, s_toolListJsonWriterOptions))
+ {
+ writer.WriteStartObject();
+ writer.WriteStartArray("tools");
+
+ foreach (Tool tool in tools)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("name", tool.Name);
+ writer.WriteString("description", tool.Description);
+ writer.WritePropertyName("inputSchema");
+ tool.InputSchema.WriteTo(writer);
+ writer.WritePropertyName("outputSchema");
+ if (tool.OutputSchema is JsonElement outputSchema)
+ {
+ outputSchema.WriteTo(writer);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+ }
+
+ return Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Length);
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs
index abfa95cc361..f9cb5cdb56a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
@@ -320,6 +321,92 @@ await handler.InvokeToolAsync(
#endregion
+ #region Reserved Tools/List Tests
+
+ [Fact]
+ public void IsListToolsToolName_WithReservedName_ShouldReturnTrue()
+ {
+ // Act
+ bool result = DefaultMcpToolHandler.IsListToolsToolName(DefaultMcpToolHandler.ListToolsToolName);
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsListToolsToolName_WithRegularToolName_ShouldReturnFalse()
+ {
+ // Act
+ bool result = DefaultMcpToolHandler.IsListToolsToolName("search");
+
+ // Assert
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task InvokeToolAsync_WithListToolsArguments_ShouldThrowArgumentExceptionAsync()
+ {
+ // Arrange
+ DefaultMcpToolHandler handler = new();
+
+ try
+ {
+ // Act
+ Func act = async () => await handler.InvokeToolAsync(
+ serverUrl: "http://localhost:12345/mcp",
+ serverLabel: "test",
+ toolName: DefaultMcpToolHandler.ListToolsToolName,
+ arguments: new Dictionary { ["ignored"] = true },
+ headers: null,
+ connectionName: null);
+
+ // Assert
+ await act.Should().ThrowAsync()
+ .WithMessage("*does not accept tool arguments*");
+ }
+ finally
+ {
+ await handler.DisposeAsync();
+ }
+ }
+
+ [Fact]
+ public async Task CreateListToolsResultContent_WithTools_ShouldSerializeToolMetadataAsync()
+ {
+ // Arrange
+ JsonElement inputSchema = JsonSerializer.Deserialize(
+ """
+ {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string"
+ }
+ },
+ "required": [ "query" ]
+ }
+ """);
+ Tool tool = new()
+ {
+ Name = "search",
+ Description = "Searches documentation.",
+ InputSchema = inputSchema
+ };
+
+ // Act
+ McpServerToolResultContent result = DefaultMcpToolHandler.CreateListToolsResultContent([tool]);
+
+ // Assert
+ TextContent text = result.Outputs.Should().ContainSingle().Subject.Should().BeOfType().Subject;
+ using JsonDocument document = JsonDocument.Parse(text.Text);
+ JsonElement listedTool = document.RootElement.GetProperty("tools")[0];
+ listedTool.GetProperty("name").GetString().Should().Be("search");
+ listedTool.GetProperty("description").GetString().Should().Be("Searches documentation.");
+ listedTool.GetProperty("inputSchema").GetProperty("properties").GetProperty("query").GetProperty("type").GetString().Should().Be("string");
+ }
+
+ #endregion
+
#region Interface Implementation Tests
[Fact]
@@ -488,5 +575,75 @@ public void ConvertContentBlock_AudioContentBlock_WithNullMimeType_ShouldDefault
dataContent.MediaType.Should().Be("audio/*");
}
+ [Fact]
+ public void ConvertContentBlock_EmbeddedResourceBlock_WithTextResource_ShouldReturnTextContent()
+ {
+ // Arrange
+ EmbeddedResourceBlock block = new()
+ {
+ Resource = new TextResourceContents
+ {
+ Text = "embedded text payload",
+ Uri = "resource://example",
+ MimeType = "text/plain",
+ },
+ };
+
+ // Act
+ AIContent result = DefaultMcpToolHandler.ConvertContentBlock(block);
+
+ // Assert
+ result.Should().BeOfType()
+ .Which.Text.Should().Be("embedded text payload");
+ }
+
+ [Fact]
+ public void ConvertContentBlock_EmbeddedResourceBlock_WithBlobResource_ShouldReturnDataContent()
+ {
+ // Arrange
+ byte[] base64Bytes = Encoding.UTF8.GetBytes("UklGRiQA");
+ EmbeddedResourceBlock block = new()
+ {
+ Resource = new BlobResourceContents
+ {
+ Blob = new ReadOnlyMemory(base64Bytes),
+ Uri = "resource://example.bin",
+ MimeType = "application/zip",
+ },
+ };
+
+ // Act
+ AIContent result = DefaultMcpToolHandler.ConvertContentBlock(block);
+
+ // Assert
+ DataContent dataContent = result.Should().BeOfType().Subject;
+ dataContent.MediaType.Should().Be("application/zip");
+ dataContent.Uri.Should().Be("data:application/zip;base64,UklGRiQA");
+ }
+
+ [Fact]
+ public void ConvertContentBlock_EmbeddedResourceBlock_WithBlobResource_NullMimeType_DefaultsToOctetStream()
+ {
+ // Arrange
+ byte[] base64Bytes = Encoding.UTF8.GetBytes("UklGRiQA");
+ EmbeddedResourceBlock block = new()
+ {
+ Resource = new BlobResourceContents
+ {
+ Blob = new ReadOnlyMemory(base64Bytes),
+ Uri = "resource://example.bin",
+ MimeType = null!,
+ },
+ };
+
+ // Act
+ AIContent result = DefaultMcpToolHandler.ConvertContentBlock(block);
+
+ // Assert
+ DataContent dataContent = result.Should().BeOfType().Subject;
+ dataContent.MediaType.Should().Be("application/octet-stream");
+ dataContent.Uri.Should().Be("data:application/octet-stream;base64,UklGRiQA");
+ }
+
#endregion
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs
index a1337b3e2dc..d047badaf75 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs
@@ -432,6 +432,44 @@ public async Task InvokeMcpToolExecuteWithNullOutputAsync()
VerifyInvocationEvent(events);
}
+ [Fact]
+ public async Task InvokeMcpToolExecuteWithReservedListToolsNameAsync()
+ {
+ // Arrange
+ this.State.InitializeSystem();
+ const string ListToolsToolName = "tools/list";
+ string? capturedToolName = null;
+ InvokeMcpTool model = this.CreateModel(
+ displayName: nameof(InvokeMcpToolExecuteWithReservedListToolsNameAsync),
+ serverUrl: TestServerUrl,
+ toolName: ListToolsToolName);
+ Mock mockProvider = new();
+ mockProvider.Setup(provider => provider.InvokeToolAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny?>(),
+ It.IsAny?>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback?, IDictionary?, string?, CancellationToken>(
+ (_, _, toolName, _, _, _, _) => capturedToolName = toolName)
+ .ReturnsAsync(new McpServerToolResultContent("list-tools-call-id")
+ {
+ Outputs = [new TextContent("{\"tools\":[]}")]
+ });
+ MockAgentProvider mockAgentProvider = new();
+ InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State);
+
+ // Act
+ WorkflowEvent[] events = await this.ExecuteAsync(action, isDiscrete: false);
+
+ // Assert
+ VerifyModel(model, action);
+ VerifyInvocationEvent(events);
+ Assert.Equal(ListToolsToolName, capturedToolName);
+ }
+
[Fact]
public async Task InvokeMcpToolExecuteWithMultipleContentTypesAsync()
{