From 8fe02b0c473f22550d80d90d0e53a7700449ef33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:38:32 +0000 Subject: [PATCH 1/8] wip: in-progress CancellationToken propagation fixes Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/Agent365-dotnet/sessions/2bd402aa-e691-40be-8866-7ab931a2f87f --- ...nfigurationService_ToolEnumerationTests.cs | 34 +++++++++++++------ .../McpToolRegistrationServiceTestBase.cs | 10 +++--- .../IMcpToolServerConfigurationService.cs | 10 ++++-- ...verConfigurationService.ToolEnumeration.cs | 13 ++++--- .../McpToolServerConfigurationService.cs | 11 +++--- .../Services/IMcpToolRegistrationService.cs | 9 +++-- .../Services/McpToolRegistrationService.cs | 12 ++++--- .../Services/IMcpToolRegistrationService.cs | 8 +++-- .../Services/McpToolRegistrationService.cs | 12 ++++--- .../Services/IMcpToolRegistrationService.cs | 3 +- .../Services/McpToolRegistrationService.cs | 4 +-- 11 files changed, 83 insertions(+), 43 deletions(-) diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/McpToolServerConfigurationService_ToolEnumerationTests.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/McpToolServerConfigurationService_ToolEnumerationTests.cs index fcffb5c8..90d6eef8 100644 --- a/src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/McpToolServerConfigurationService_ToolEnumerationTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/McpToolServerConfigurationService_ToolEnumerationTests.cs @@ -10,6 +10,7 @@ using ModelContextProtocol.Client; using Moq; using System.Net.Http; +using System.Threading; using Xunit; namespace Microsoft.Agents.A365.Tooling.Core.Tests; @@ -109,7 +110,8 @@ public async Task EnumerateToolsFromServersAsync_FiltersInvalidServers_WithMissi It.IsAny(), It.Is(s => s.mcpServerName == "valid-server"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(new List()); // Act @@ -145,7 +147,8 @@ public async Task EnumerateToolsFromServersAsync_FiltersInvalidServers_WithMissi It.IsAny(), It.Is(s => s.mcpServerName == "valid-server"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(new List()); // Act @@ -183,7 +186,8 @@ public async Task EnumerateToolsFromServersAsync_EnumeratesToolsFromMultipleServ It.IsAny(), It.Is(s => s.mcpServerName == "server1"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(tools1); _mockService @@ -191,7 +195,8 @@ public async Task EnumerateToolsFromServersAsync_EnumeratesToolsFromMultipleServ It.IsAny(), It.Is(s => s.mcpServerName == "server2"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(tools2); // Act @@ -231,7 +236,8 @@ public async Task EnumerateToolsFromServersAsync_HandlesIndividualServerFailures It.IsAny(), It.Is(s => s.mcpServerName == "failing-server"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ThrowsAsync(new Exception("Server connection failed")); _mockService @@ -239,7 +245,8 @@ public async Task EnumerateToolsFromServersAsync_HandlesIndividualServerFailures It.IsAny(), It.Is(s => s.mcpServerName == "working-server"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(workingTools); // Act @@ -282,7 +289,8 @@ public async Task EnumerateToolsFromServersAsync_EnumeratesInParallel() It.IsAny(), It.Is(s => s.mcpServerName == "server1"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .Returns(async () => { lock (callOrder) callOrder.Add("server1-start"); @@ -296,7 +304,8 @@ public async Task EnumerateToolsFromServersAsync_EnumeratesInParallel() It.IsAny(), It.Is(s => s.mcpServerName == "server2"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .Returns(async () => { lock (callOrder) callOrder.Add("server2-start"); @@ -310,7 +319,8 @@ public async Task EnumerateToolsFromServersAsync_EnumeratesInParallel() It.IsAny(), It.Is(s => s.mcpServerName == "server3"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .Returns(async () => { lock (callOrder) callOrder.Add("server3-start"); @@ -380,7 +390,8 @@ public async Task EnumerateAllToolsAsync_ReturnsFlatListOfAllTools() It.IsAny(), It.Is(s => s.mcpServerName == "server1"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(tools1); _mockService @@ -388,7 +399,8 @@ public async Task EnumerateAllToolsAsync_ReturnsFlatListOfAllTools() It.IsAny(), It.Is(s => s.mcpServerName == "server2"), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync(tools2); // Act diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/McpToolRegistrationServiceTestBase.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/McpToolRegistrationServiceTestBase.cs index 80c7c975..9d84c58f 100644 --- a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/McpToolRegistrationServiceTestBase.cs +++ b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/McpToolRegistrationServiceTestBase.cs @@ -78,11 +78,12 @@ protected void SetupMocksForAddToolServers(Action? captureToolOptio It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())); + It.IsAny(), + It.IsAny())); if (captureToolOptions != null) { - setup.Callback((_, _, _, options) => captureToolOptions(options)); + setup.Callback((_, _, _, options, _) => captureToolOptions(options)); } setup.ReturnsAsync((new List(), new Dictionary>())); @@ -102,11 +103,12 @@ protected void SetupMocksForGetMcpTools(Action? captureToolOptions It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())); + It.IsAny(), + It.IsAny())); if (captureToolOptions != null) { - setup.Callback((_, _, _, options) => captureToolOptions(options)); + setup.Callback((_, _, _, options, _) => captureToolOptions(options)); } setup.ReturnsAsync(new List()); diff --git a/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs b/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs index fe5a4ff9..664e48b4 100644 --- a/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs +++ b/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs @@ -5,6 +5,7 @@ using Microsoft.Agents.A365.Tooling.Models; using Microsoft.Agents.Builder; using ModelContextProtocol.Client; +using System.Threading; namespace Microsoft.Agents.A365.Tooling.Services { @@ -37,9 +38,10 @@ public interface IMcpToolServerConfigurationService /// The MCP server configuration. /// The authentication token. /// Tool options for listing servers. + /// A cancellation token to cancel the operation. /// MCP Client Tools /// - Task> GetMcpClientToolsAsync(ITurnContext turnContext, MCPServerConfig mCPServerConfig, string authToken, ToolOptions toolOptions); + Task> GetMcpClientToolsAsync(ITurnContext turnContext, MCPServerConfig mCPServerConfig, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken = default); /// /// Sends chat history to the MCP platform for real-time threat protection. @@ -81,8 +83,9 @@ public interface IMcpToolServerConfigurationService /// Authentication token for MCP server access. /// Turn context for the current request. /// Tool options including user agent configuration. + /// A cancellation token to cancel the operation. /// A tuple containing server configurations and a dictionary mapping server names to their available tools. - Task<(List Servers, Dictionary> ToolsByServer)> EnumerateToolsFromServersAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions); + Task<(List Servers, Dictionary> ToolsByServer)> EnumerateToolsFromServersAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions, CancellationToken cancellationToken = default); /// /// Enumerates all MCP tools from configured servers, returning a flat list of all tools. @@ -91,7 +94,8 @@ public interface IMcpToolServerConfigurationService /// Authentication token for MCP server access. /// Turn context for the current request. /// Tool options including user agent configuration. + /// A cancellation token to cancel the operation. /// A flat list of all MCP tools from all configured servers. - Task> EnumerateAllToolsAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions); + Task> EnumerateAllToolsAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions, CancellationToken cancellationToken = default); } } diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs index 57c94d95..68020ef2 100644 --- a/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs +++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs @@ -9,6 +9,7 @@ namespace Microsoft.Agents.A365.Tooling.Services using ModelContextProtocol.Client; using System; using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; /// @@ -21,7 +22,8 @@ public partial class McpToolServerConfigurationService string agentInstanceId, string authToken, ITurnContext turnContext, - ToolOptions toolOptions) + ToolOptions toolOptions, + CancellationToken cancellationToken = default) { var toolsByServer = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -68,7 +70,8 @@ public partial class McpToolServerConfigurationService turnContext, server, authToken, - toolOptions).ConfigureAwait(false); + toolOptions, + cancellationToken).ConfigureAwait(false); _logger.LogInformation( "Successfully loaded {ToolCount} tools from MCP server '{ServerName}'", @@ -111,13 +114,15 @@ public virtual async Task> EnumerateAllToolsAsync( string agentInstanceId, string authToken, ITurnContext turnContext, - ToolOptions toolOptions) + ToolOptions toolOptions, + CancellationToken cancellationToken = default) { var (_, toolsByServer) = await EnumerateToolsFromServersAsync( agentInstanceId, authToken, turnContext, - toolOptions).ConfigureAwait(false); + toolOptions, + cancellationToken).ConfigureAwait(false); var allTools = new List(); foreach (var tools in toolsByServer.Values) diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs index db26decb..1f39fc97 100644 --- a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs +++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs @@ -65,7 +65,8 @@ public virtual async Task> GetMcpClientToolsAsync( ITurnContext turnContext, MCPServerConfig mCPServerConfig, string authToken, - ToolOptions toolOptions) + ToolOptions toolOptions, + CancellationToken cancellationToken = default) { try { @@ -78,8 +79,8 @@ public virtual async Task> GetMcpClientToolsAsync( this._logger.LogInformation($"Creating custom MCP client for: {mCPServerConfig.mcpServerName} at {mCPServerConfig.url}"); // Use custom HTTP-based implementation since MCP client library doesn't work - var mcpClient = await CreateMcpClientWithAuthHandlers(turnContext, new Uri(mCPServerConfig.url), authToken, toolOptions); - var tools = await mcpClient.ListToolsAsync(); + var mcpClient = await CreateMcpClientWithAuthHandlers(turnContext, new Uri(mCPServerConfig.url), authToken, toolOptions, cancellationToken); + var tools = await mcpClient.ListToolsAsync(cancellationToken: cancellationToken); this._logger.LogInformation($"Successfully retrieved {tools.Count} tools from {mCPServerConfig.mcpServerName}"); @@ -459,7 +460,7 @@ private List GetMCPServersFromManifest() /// /// Creates an MCP client with authentication handlers similar to your reference implementation /// - private async Task CreateMcpClientWithAuthHandlers(ITurnContext turnContext, Uri endpoint, string authToken, ToolOptions toolOptions) + private async Task CreateMcpClientWithAuthHandlers(ITurnContext turnContext, Uri endpoint, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken = default) { // Create HTTP client handler chain for MCP service authentication var httpClientHandler = new HttpClientHandler(); @@ -507,7 +508,7 @@ private async Task CreateMcpClientWithAuthHandlers(ITurnContext turn try { - return await McpClientFactory.CreateAsync(clientTransport, loggerFactory: this._loggerFactory); + return await McpClientFactory.CreateAsync(clientTransport, loggerFactory: this._loggerFactory, cancellationToken: cancellationToken); } catch (Exception ex) { diff --git a/src/Tooling/Extensions/AgentFramework/Services/IMcpToolRegistrationService.cs b/src/Tooling/Extensions/AgentFramework/Services/IMcpToolRegistrationService.cs index aaf2f087..454e88b9 100644 --- a/src/Tooling/Extensions/AgentFramework/Services/IMcpToolRegistrationService.cs +++ b/src/Tooling/Extensions/AgentFramework/Services/IMcpToolRegistrationService.cs @@ -10,6 +10,7 @@ namespace Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services; using Microsoft.Agents.Builder.App.UserAuth; using Microsoft.Extensions.AI; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; /// @@ -32,6 +33,7 @@ public interface IMcpToolRegistrationService /// User authorization information /// Authentication Handler Name for use with the UserAuthorization System /// Optional auth token to access the MCP servers. + /// A cancellation token to cancel the operation. /// New Agent instance with all MCP tools, or agent with original tools if no new servers Task AddToolServersToAgent( IChatClient chatClient, @@ -41,7 +43,8 @@ Task AddToolServersToAgent( UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null); + string? authToken = null, + CancellationToken cancellationToken = default); /// /// Returns a List of MCP tools to be added to the agent. @@ -51,13 +54,15 @@ Task AddToolServersToAgent( /// User authorization information /// Authentication Handler Name for use with the UserAuthorization System /// Optional auth token to access the MCP servers. + /// A cancellation token to cancel the operation. /// List of AI Tools be added to an agent. Task> GetMcpToolsAsync( string agentUserId, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null); + string? authToken = null, + CancellationToken cancellationToken = default); /// /// Sends chat history to the MCP platform. diff --git a/src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs b/src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs index 10927ab1..5eefacab 100644 --- a/src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs +++ b/src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs @@ -53,7 +53,8 @@ public async Task AddToolServersToAgent( UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null) + string? authToken = null, + CancellationToken cancellationToken = default) { if (chatClient == null) { @@ -83,7 +84,8 @@ public async Task AddToolServersToAgent( agentUserId, authToken, turnContext, - toolOptions).ConfigureAwait(false); + toolOptions, + cancellationToken).ConfigureAwait(false); // Add all MCP tools from all servers foreach (var serverEntry in toolsByServer) @@ -116,7 +118,8 @@ public async Task> GetMcpToolsAsync( UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null) + string? authToken = null, + CancellationToken cancellationToken = default) { try { @@ -135,7 +138,8 @@ public async Task> GetMcpToolsAsync( agentUserId, authToken, turnContext, - toolOptions).ConfigureAwait(false); + toolOptions, + cancellationToken).ConfigureAwait(false); // Convert to AITool list var a365ToolList = mcpTools.Cast().ToList(); diff --git a/src/Tooling/Extensions/AzureAIFoundry/Services/IMcpToolRegistrationService.cs b/src/Tooling/Extensions/AzureAIFoundry/Services/IMcpToolRegistrationService.cs index 2623c9a1..e30d2cff 100644 --- a/src/Tooling/Extensions/AzureAIFoundry/Services/IMcpToolRegistrationService.cs +++ b/src/Tooling/Extensions/AzureAIFoundry/Services/IMcpToolRegistrationService.cs @@ -26,12 +26,14 @@ public interface IMcpToolRegistrationService /// Authentication Handler Name for use with the UserAuthorization System /// Turn context for the conversation. /// Optional auth token to access the MCP servers. + /// A cancellation token to cancel the operation. Task AddToolServersToAgentAsync( PersistentAgentsClient agentClient, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null); + string? authToken = null, + CancellationToken cancellationToken = default); /// /// Get MCP tool definitions and resources asynchronously. @@ -39,11 +41,13 @@ Task AddToolServersToAgentAsync( /// Agent Instance Id for the agent. /// Auth token to access the MCP servers. /// Turn context for the conversation. + /// A cancellation token to cancel the operation. /// A tuple containing the list of MCP tool definitions and tool resources. Task<(IList ToolDefinitions, ToolResources? ToolResources)> GetMcpToolDefinitionsAndResourcesAsync( string agentInstanceId, string authToken, - ITurnContext turnContext); + ITurnContext turnContext, + CancellationToken cancellationToken = default); /// /// Sends chat history to the MCP platform for real-time threat protection. diff --git a/src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs b/src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs index 406e160d..d0d62499 100644 --- a/src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs +++ b/src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs @@ -97,7 +97,8 @@ public async Task AddToolServersToAgentAsync( UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null) + string? authToken = null, + CancellationToken cancellationToken = default) { if (agentClient == null) { @@ -113,8 +114,7 @@ public async Task AddToolServersToAgentAsync( try { - // Perform the (potentially async) work in a dedicated task to keep this synchronous signature. - var (toolDefinitions, toolResources) = GetMcpToolDefinitionsAndResourcesAsync(agenticAppId, authToken ?? string.Empty, turnContext).GetAwaiter().GetResult(); + var (toolDefinitions, toolResources) = await GetMcpToolDefinitionsAndResourcesAsync(agenticAppId, authToken ?? string.Empty, turnContext, cancellationToken).ConfigureAwait(false); agentClient.Administration.UpdateAgent( agenticAppId, @@ -136,7 +136,8 @@ public async Task AddToolServersToAgentAsync( public async Task<(IList ToolDefinitions, ToolResources? ToolResources)> GetMcpToolDefinitionsAndResourcesAsync( string agentInstanceId, string authToken, - ITurnContext turnContext) + ITurnContext turnContext, + CancellationToken cancellationToken = default) { // TODO: Make this method private // Tool resources should ideally be accessible via agentClient after AddToolServersToAgent. @@ -153,7 +154,8 @@ public async Task AddToolServersToAgentAsync( agentInstanceId, authToken, turnContext, - toolOptions).ConfigureAwait(false); + toolOptions, + cancellationToken).ConfigureAwait(false); if (servers.Count == 0) { diff --git a/src/Tooling/Extensions/SemanticKernel/Services/IMcpToolRegistrationService.cs b/src/Tooling/Extensions/SemanticKernel/Services/IMcpToolRegistrationService.cs index d31aa8ac..e133e8f5 100644 --- a/src/Tooling/Extensions/SemanticKernel/Services/IMcpToolRegistrationService.cs +++ b/src/Tooling/Extensions/SemanticKernel/Services/IMcpToolRegistrationService.cs @@ -25,9 +25,10 @@ public interface IMcpToolRegistrationService /// Authentication Handler Name for use with the UserAuthorization System /// /// Auth token to access the MCP servers + /// A cancellation token to cancel the operation. /// Returns a new object of the kernel /// - Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken = null); + Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken = null, CancellationToken cancellationToken = default); /// /// Sends chat history to the MCP platform for real-time threat protection. diff --git a/src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs b/src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs index c59575fb..18a1fbc0 100644 --- a/src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs +++ b/src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs @@ -48,7 +48,7 @@ public McpToolRegistrationService( } /// - public async Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken = null) + public async Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken = null, CancellationToken cancellationToken = default) { if (kernel == null) { @@ -68,7 +68,7 @@ public async Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization us UserAgentConfiguration = Agent365SemanticKernelSdkUserAgentConfiguration.Instance }; - var (_, toolsByServer) = await _mcpServerConfigurationService.EnumerateToolsFromServersAsync(agenticAppId, authToken, turnContext, toolOptions).ConfigureAwait(false); + var (_, toolsByServer) = await _mcpServerConfigurationService.EnumerateToolsFromServersAsync(agenticAppId, authToken, turnContext, toolOptions, cancellationToken).ConfigureAwait(false); foreach (var serverEntry in toolsByServer) { From fb7d5b0ca42cd674c1cca971edf4350ae68f45d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:51:02 +0000 Subject: [PATCH 2/8] fix: propagate CancellationToken through async MCP tooling and observability call chains (fixes #223) Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/Agent365-dotnet/sessions/2bd402aa-e691-40be-8866-7ab931a2f87f --- .../Hosting/Caching/AgenticTokenCache.cs | 3 +- .../Hosting/Caching/IExporterTokenCache.cs | 3 +- .../Hosting/Caching/ServiceTokenCache.cs | 11 +- ...bservabilityServiceCollectionExtensions.cs | 4 +- .../Tracing/Exporters/Agent365Exporter.cs | 2 +- .../Exporters/Agent365ExporterAsync.cs | 5 +- .../Tracing/Exporters/Agent365ExporterCore.cs | 9 +- .../Exporters/Agent365ExporterOptions.cs | 3 +- ...abilityServiceCollectionExtensionsTests.cs | 5 +- .../ObservabilityBuilderExtensionsTests.cs | 4 +- .../Agent365ExporterAsyncE2ETests.cs | 832 +++++++++--------- .../Agent365ExporterE2ETests.cs | 4 +- .../BuilderTests.cs | 8 +- .../Exporters/Agent365ExporterTests.cs | 70 +- .../ObservabilityBuilderExtensionsTests.cs | 4 +- ...nfigurationService_ToolEnumerationTests.cs | 18 +- .../AddToolServersToAgent_Tests.cs | 3 +- .../GetMcpToolsAsync_Tests.cs | 3 +- .../McpToolRegistrationServiceTests.cs | 11 +- .../McpToolRegistrationServiceTests.cs | 8 +- .../IMcpToolServerConfigurationService.cs | 6 +- ...verConfigurationService.ToolEnumeration.cs | 3 +- .../McpToolServerConfigurationService.cs | 13 +- 23 files changed, 525 insertions(+), 507 deletions(-) diff --git a/src/Observability/Hosting/Caching/AgenticTokenCache.cs b/src/Observability/Hosting/Caching/AgenticTokenCache.cs index bfe964e4..cf086e86 100644 --- a/src/Observability/Hosting/Caching/AgenticTokenCache.cs +++ b/src/Observability/Hosting/Caching/AgenticTokenCache.cs @@ -93,10 +93,11 @@ public void RegisterObservability(string agentId, string tenantId, AgenticTokenS /// /// The agent identifier. /// The tenant identifier. + /// A cancellation token to cancel the operation. /// /// The observability token if available; otherwise, null. /// - public async Task GetObservabilityToken(string agentId, string tenantId) + public async Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken = default) { if (!_map.TryGetValue($"{agentId}:{tenantId}", out var entry)) return null; diff --git a/src/Observability/Hosting/Caching/IExporterTokenCache.cs b/src/Observability/Hosting/Caching/IExporterTokenCache.cs index f2ac5e5c..b4d60c48 100644 --- a/src/Observability/Hosting/Caching/IExporterTokenCache.cs +++ b/src/Observability/Hosting/Caching/IExporterTokenCache.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Agents.A365.Observability.Hosting.Caching @@ -17,6 +18,6 @@ public interface IExporterTokenCache where T : class /// /// Returns an observability token (cached inside the credential) or null on failure/not registered. /// - Task GetObservabilityToken(string agentId, string tenantId); + Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/Observability/Hosting/Caching/ServiceTokenCache.cs b/src/Observability/Hosting/Caching/ServiceTokenCache.cs index fe94a0bc..76f7c7fd 100644 --- a/src/Observability/Hosting/Caching/ServiceTokenCache.cs +++ b/src/Observability/Hosting/Caching/ServiceTokenCache.cs @@ -121,16 +121,17 @@ public void RegisterObservability(string agentId, string tenantId, string token, /// /// The agent identifier. /// The tenant identifier. + /// A cancellation token to cancel the operation. /// The observability token if valid; otherwise, null. - public async Task GetObservabilityToken(string agentId, string tenantId) + public Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(agentId) || string.IsNullOrWhiteSpace(tenantId)) - return null; + return Task.FromResult(null); var key = GetKey(agentId, tenantId); if (!_map.TryGetValue(key, out var entry)) - return null; + return Task.FromResult(null); // Check if token has expired if (DateTimeOffset.UtcNow >= entry.ExpiresAt) @@ -140,10 +141,10 @@ public void RegisterObservability(string agentId, string tenantId, string token, { removedEntry.ClearToken(); } - return null; + return Task.FromResult(null); } - return await Task.FromResult(entry.Token).ConfigureAwait(false); + return Task.FromResult(entry.Token); } /// diff --git a/src/Observability/Hosting/ObservabilityServiceCollectionExtensions.cs b/src/Observability/Hosting/ObservabilityServiceCollectionExtensions.cs index 7e3875c0..04569ceb 100644 --- a/src/Observability/Hosting/ObservabilityServiceCollectionExtensions.cs +++ b/src/Observability/Hosting/ObservabilityServiceCollectionExtensions.cs @@ -27,7 +27,7 @@ public static IServiceCollection AddAgenticTracingExporter(this IServiceCollecti return new Agent365ExporterOptions { ClusterCategory = clusterCategory ?? "production", - TokenResolver = async (agentId, tenantId) => await cache.GetObservabilityToken(agentId, tenantId) + TokenResolver = (agentId, tenantId, ct) => cache.GetObservabilityToken(agentId, tenantId, ct) }; }); @@ -51,7 +51,7 @@ public static IServiceCollection AddServiceTracingExporter(this IServiceCollecti return new Agent365ExporterOptions { ClusterCategory = clusterCategory ?? "production", - TokenResolver = async (agentId, tenantId) => await cache.GetObservabilityToken(agentId, tenantId).ConfigureAwait(false), + TokenResolver = (agentId, tenantId, ct) => cache.GetObservabilityToken(agentId, tenantId, ct), UseS2SEndpoint = true // Service-to-service uses S2S endpoint }; }); diff --git a/src/Observability/Runtime/Tracing/Exporters/Agent365Exporter.cs b/src/Observability/Runtime/Tracing/Exporters/Agent365Exporter.cs index 4ad62bde..a389a959 100644 --- a/src/Observability/Runtime/Tracing/Exporters/Agent365Exporter.cs +++ b/src/Observability/Runtime/Tracing/Exporters/Agent365Exporter.cs @@ -72,7 +72,7 @@ public override ExportResult Export(in Batch batch) groups: groups, resource: _resource, options: _options, - tokenResolver: (agentId, tenantId) => _options.TokenResolver!(agentId, tenantId), + tokenResolver: (agentId, tenantId, ct) => _options.TokenResolver!(agentId, tenantId, ct), sendAsync: request => _httpClient.SendAsync(request) ).GetAwaiter().GetResult(); } diff --git a/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterAsync.cs b/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterAsync.cs index 3eccfcab..989ce349 100644 --- a/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterAsync.cs +++ b/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterAsync.cs @@ -74,8 +74,9 @@ await _core.ExportBatchCoreAsync( groups: groups, resource: this._resource, options: this._options, - tokenResolver: (agentId, tenantId) => this._options.TokenResolver!(agentId, tenantId), - sendAsync: request => this._httpClient.SendAsync(request, cancellationToken) + tokenResolver: (agentId, tenantId, ct) => this._options.TokenResolver!(agentId, tenantId, ct), + sendAsync: request => this._httpClient.SendAsync(request, cancellationToken), + cancellationToken: cancellationToken ).ConfigureAwait(false); } catch (OperationCanceledException) diff --git a/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterCore.cs b/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterCore.cs index 387e7db7..43187cab 100644 --- a/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterCore.cs +++ b/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterCore.cs @@ -13,6 +13,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters @@ -127,13 +128,15 @@ public string BuildRequestUri(string endpoint, string endpointPath) /// /// /// + /// A cancellation token to cancel the operation. /// public async Task ExportBatchCoreAsync( IEnumerable<(string TenantId, string AgentId, List Activities)> groups, Resource resource, Agent365ExporterOptions options, - Func> tokenResolver, - Func> sendAsync) + Func> tokenResolver, + Func> sendAsync, + CancellationToken cancellationToken = default) { foreach (var g in groups) { @@ -157,7 +160,7 @@ public async Task ExportBatchCoreAsync( string? token = null; try { - token = await tokenResolver(agentId, tenantId).ConfigureAwait(false); + token = await tokenResolver(agentId, tenantId, cancellationToken).ConfigureAwait(false); this._logger?.LogDebug("Agent365ExporterCore: Obtained token for agent {AgentId} tenant {TenantId}.", agentId, tenantId); } catch (Exception ex) diff --git a/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterOptions.cs b/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterOptions.cs index e692576f..38d5c862 100644 --- a/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterOptions.cs +++ b/src/Observability/Runtime/Tracing/Exporters/Agent365ExporterOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters @@ -10,7 +11,7 @@ namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters /// Must be fast and non-blocking (use internal caching elsewhere). /// Return null/empty to omit the Authorization header. /// - public delegate Task AsyncAuthTokenResolver(string agentId, string tenantId); + public delegate Task AsyncAuthTokenResolver(string agentId, string tenantId, CancellationToken cancellationToken = default); /// /// Delegate used by the exporter to resolve the endpoint host or URL for a given tenant id. diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Extensions/ObservabilityServiceCollectionExtensionsTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Extensions/ObservabilityServiceCollectionExtensionsTests.cs index b8671c5f..9df03fe3 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Extensions/ObservabilityServiceCollectionExtensionsTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Extensions/ObservabilityServiceCollectionExtensionsTests.cs @@ -121,7 +121,7 @@ public void AddServiceTracingExporter_TokenResolver_CanBeCalled() var options = serviceProvider.GetRequiredService(); // Act - var token = options.TokenResolver!("test-agent", "test-tenant"); + var token = options.TokenResolver!("test-agent", "test-tenant", default); // Assert // Token resolver should not throw (actual token retrieval logic is in the cache) @@ -139,8 +139,7 @@ public void AddAgenticTracingExporter_TokenResolver_CanBeCalled() var options = serviceProvider.GetRequiredService(); // Act - var token = options.TokenResolver!("test-agent", "test-tenant"); - + var token = options.TokenResolver!("test-agent", "test-tenant", default); // Assert // Token resolver should not throw (actual token retrieval logic is in the cache) // This just verifies the resolver is wired up diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/ObservabilityBuilderExtensionsTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/ObservabilityBuilderExtensionsTests.cs index 6161d7e9..93751f4d 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/ObservabilityBuilderExtensionsTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/ObservabilityBuilderExtensionsTests.cs @@ -83,7 +83,7 @@ public void AddA365Tracing_WithOpenTelemetryBuilderTrue_AndExporterEnabled_Regis builder.Services.AddSingleton(sp => new Agent365ExporterOptions { UseS2SEndpoint = false, - TokenResolver = (_, _) => Task.FromResult("test-token") + TokenResolver = (_, _, _) => Task.FromResult("test-token") }); }); webHostBuilder.UseStartup(); @@ -160,7 +160,7 @@ public void AddA365Tracing_IHostBuilder_WithOpenTelemetryBuilderTrue_AndExporter builder.Services.AddSingleton(_ => new Agent365ExporterOptions { UseS2SEndpoint = false, - TokenResolver = (_, _) => Task.FromResult("test-token") + TokenResolver = (_, _, _) => Task.FromResult("test-token") }); }); diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs index 4ff51b23..125675dc 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs @@ -1,416 +1,416 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using FluentAssertions; -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters; -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System.Net; -using System.Text.Json; - -namespace Microsoft.Agents.A365.Observability.Runtime.Tests.IntegrationTests -{ - [TestClass] - public class Agent365ExporterAsyncE2ETests - { - private TestHttpMessageHandler? _handler; - private ServiceProvider? _provider; - private bool _receivedRequest; - private string? _receivedContent; - - [TestMethod] - public async Task AddTracing_And_InvokeAgentScope_ExporterMakesExpectedRequest() - { - // Arrange - this.SetupExporterTest(); - this._receivedRequest = false; - this._receivedContent = null; - var expectedAgentType = AgentType.EntraEmbodied; - var expectedAgentDetails = new AgentDetails( - agentId: Guid.NewGuid().ToString(), - agentName: "Test Agent", - agentDescription: "Agent for testing.", - agentAUID: Guid.NewGuid().ToString(), - agentUPN: "testagent@ztaitest12.onmicrosoft.com", - agentBlueprintId: Guid.NewGuid().ToString(), - tenantId: Guid.NewGuid().ToString(), - agentType: expectedAgentType); - var endpoint = new Uri("https://test-agent-endpoint"); - var invokeAgentDetails = new InvokeAgentDetails(endpoint: endpoint, details: expectedAgentDetails); - var tenantDetails = new TenantDetails(Guid.NewGuid()); - - var expectedRequest = new Request( - content: "Test request content", - executionType: ExecutionType.HumanToAgent, - channel: new Channel( - name: "msteams", - link: "https://testchannel.link")); - - var expectedCallerDetails = new CallerDetails( - callerId: "caller-123", - callerName: "Test Caller", - callerUpn: "caller-123@ztaitest12.onmicrosoft.com", - callerClientIP: IPAddress.Parse("203.0.113.42"), - tenantId: expectedAgentDetails.TenantId); - - // Act - using (var scope = InvokeAgentScope.Start( - invokeAgentDetails: invokeAgentDetails, - tenantDetails: tenantDetails, - request: expectedRequest, - callerDetails: expectedCallerDetails)) - { - scope.RecordInputMessages(new[] { "Input message 1", "Input message 2" }); - scope.RecordOutputMessages(new[] { "Output message 1" }); - } - - var timeout = TimeSpan.FromSeconds(30); - var start = DateTime.UtcNow; - while (!this._receivedRequest && DateTime.UtcNow - start < timeout) - { - await Task.Delay(1000).ConfigureAwait(false); - } - - this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); - this._receivedContent.Should().NotBeNull("Exporter should send a request body."); - - using var doc = JsonDocument.Parse(this._receivedContent!); - var root = doc.RootElement; - - var attributes = root - .GetProperty("resourceSpans")[0] - .GetProperty("scopeSpans")[0] - .GetProperty("spans")[0] - .GetProperty("attributes"); - this.GetAttribute(attributes, "server.address").Should().Be(invokeAgentDetails.Endpoint?.Host); - this.GetAttribute(attributes, "microsoft.channel.name").Should().Be(expectedRequest.Channel?.Name); - this.GetAttribute(attributes, "microsoft.channel.link").Should().Be(expectedRequest.Channel?.Link); - this.GetAttribute(attributes, "microsoft.tenant.id").Should().Be(tenantDetails.TenantId.ToString()); - this.GetAttribute(attributes, "user.id").Should().Be(expectedCallerDetails.CallerId); - this.GetAttribute(attributes, "user.email").Should().Be(expectedCallerDetails.CallerUpn); - this.GetAttribute(attributes, "user.name").Should().Be(expectedCallerDetails.CallerName); - this.GetAttribute(attributes, "gen_ai.input.messages").Should().Be("Input message 1,Input message 2"); - this.GetAttribute(attributes, "gen_ai.output.messages").Should().Be("Output message 1"); - this.GetAttribute(attributes, "gen_ai.agent.id").Should().Be(expectedAgentDetails.AgentId); - this.GetAttribute(attributes, "gen_ai.agent.name").Should().Be(expectedAgentDetails.AgentName); - this.GetAttribute(attributes, "gen_ai.agent.description").Should().Be(expectedAgentDetails.AgentDescription); - this.GetAttribute(attributes, "microsoft.agent.user.id").Should().Be(expectedAgentDetails.AgentAUID); - this.GetAttribute(attributes, "microsoft.agent.user.email").Should().Be(expectedAgentDetails.AgentUPN); - this.GetAttribute(attributes, "microsoft.a365.agent.blueprint.id").Should().Be(expectedAgentDetails.AgentBlueprintId); - this.GetAttribute(attributes, "microsoft.tenant.id").Should().Be(tenantDetails.TenantId.ToString()); - this.GetAttribute(attributes, "gen_ai.operation.name").Should().Be("invoke_agent"); - } - - [TestMethod] - public async Task AddTracing_And_ExecuteToolScope_ExporterMakesExpectedRequest() - { - // Arrange - this.SetupExporterTest(); - this._receivedRequest = false; - this._receivedContent = null; - var expectedAgentType = AgentType.EntraEmbodied; - var expectedAgentDetails = new AgentDetails( - agentId: Guid.NewGuid().ToString(), - agentName: "Tool Agent", - agentDescription: "Agent for tool execution.", - agentAUID: Guid.NewGuid().ToString(), - agentUPN: "toolagent@ztaitest12.onmicrosoft.com", - agentBlueprintId: Guid.NewGuid().ToString(), - tenantId: Guid.NewGuid().ToString(), - agentType: expectedAgentType); - var tenantDetails = new TenantDetails(Guid.NewGuid()); - var endpoint = new Uri("https://tool-endpoint:8443"); - var toolCallDetails = new ToolCallDetails( - toolName: "TestTool", - arguments: "{\"param\":\"value\"}", - toolCallId: "call-456", - description: "Test tool call description", - toolType: "custom-type", - endpoint: endpoint); - - // Act - using (var scope = ExecuteToolScope.Start(toolCallDetails, expectedAgentDetails, tenantDetails)) - { - scope.RecordResponse("Tool response content"); - } - - var timeout = TimeSpan.FromSeconds(30); - var start = DateTime.UtcNow; - while (!this._receivedRequest && DateTime.UtcNow - start < timeout) - { - await Task.Delay(1000).ConfigureAwait(false); - } - - this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); - this._receivedContent.Should().NotBeNull("Exporter should send a request body."); - - using var doc = JsonDocument.Parse(this._receivedContent!); - var root = doc.RootElement; - - var attributes = root - .GetProperty("resourceSpans")[0] - .GetProperty("scopeSpans")[0] - .GetProperty("spans")[0] - .GetProperty("attributes"); - - this.GetAttribute(attributes, "gen_ai.operation.name").Should().Be("execute_tool"); - this.GetAttribute(attributes, "gen_ai.agent.id").Should().Be(expectedAgentDetails.AgentId); - this.GetAttribute(attributes, "gen_ai.agent.name").Should().Be(expectedAgentDetails.AgentName); - this.GetAttribute(attributes, "gen_ai.agent.description").Should().Be(expectedAgentDetails.AgentDescription); - this.GetAttribute(attributes, "microsoft.agent.user.id").Should().Be(expectedAgentDetails.AgentAUID); - this.GetAttribute(attributes, "microsoft.agent.user.email").Should().Be(expectedAgentDetails.AgentUPN); - this.GetAttribute(attributes, "microsoft.a365.agent.blueprint.id").Should().Be(expectedAgentDetails.AgentBlueprintId); - this.GetAttribute(attributes, "microsoft.tenant.id").Should().Be(tenantDetails.TenantId.ToString()); - this.GetAttribute(attributes, "gen_ai.tool.name").Should().Be(toolCallDetails.ToolName); - this.GetAttribute(attributes, "gen_ai.tool.arguments").Should().Be(toolCallDetails.Arguments); - this.GetAttribute(attributes, "gen_ai.tool.call.id").Should().Be(toolCallDetails.ToolCallId); - this.GetAttribute(attributes, "gen_ai.tool.description").Should().Be(toolCallDetails.Description); - this.GetAttribute(attributes, "gen_ai.tool.type").Should().Be(toolCallDetails.ToolType); - this.GetAttribute(attributes, "server.address").Should().Be(endpoint.Host); - this.GetAttribute(attributes, "server.port").Should().Be(endpoint.Port.ToString()); - this.GetAttribute(attributes, "gen_ai.tool.call.result").Should().Be("Tool response content"); - } - - [TestMethod] - public async Task AddTracing_And_InferenceScope_ExporterMakesExpectedRequest() - { - // Arrange - this.SetupExporterTest(); - this._receivedRequest = false; - this._receivedContent = null; - var expectedAgentType = AgentType.EntraEmbodied; - var expectedAgentDetails = new AgentDetails( - agentId: Guid.NewGuid().ToString(), - agentName: "Inference Agent", - agentDescription: "Agent for inference testing.", - agentAUID: Guid.NewGuid().ToString(), - agentUPN: "inferenceagent@ztaitest12.onmicrosoft.com", - agentBlueprintId: Guid.NewGuid().ToString(), - tenantId: Guid.NewGuid().ToString(), - agentType: expectedAgentType); - var tenantDetails = new TenantDetails(Guid.NewGuid()); - - var inferenceDetails = new InferenceCallDetails( - operationName: InferenceOperationType.Chat, - model: "gpt-4", - providerName: "OpenAI", - inputTokens: 42, - outputTokens: 84, - finishReasons: new[] { "stop", "length" }, - responseId: "response-xyz"); - - // Act - using (var scope = InferenceScope.Start(inferenceDetails, expectedAgentDetails, tenantDetails)) - { - scope.RecordInputMessages(new[] { "Hello", "World" }); - scope.RecordOutputMessages(new[] { "Hi there!" }); - scope.RecordInputTokens(42); - scope.RecordOutputTokens(84); - scope.RecordFinishReasons(new[] { "stop", "length" }); - } - - var timeout = TimeSpan.FromSeconds(30); - var start = DateTime.UtcNow; - while (!this._receivedRequest && DateTime.UtcNow - start < timeout) - { - await Task.Delay(1000).ConfigureAwait(false); - } - - this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); - this._receivedContent.Should().NotBeNull("Exporter should send a request body."); - - using var doc = JsonDocument.Parse(this._receivedContent!); - var root = doc.RootElement; - var attributes = root - .GetProperty("resourceSpans")[0] - .GetProperty("scopeSpans")[0] - .GetProperty("spans")[0] - .GetProperty("attributes"); - - this.GetAttribute(attributes, "gen_ai.operation.name").Should().Be(inferenceDetails.OperationName.ToString()); - this.GetAttribute(attributes, "gen_ai.agent.id").Should().Be(expectedAgentDetails.AgentId); - this.GetAttribute(attributes, "gen_ai.agent.name").Should().Be(expectedAgentDetails.AgentName); - this.GetAttribute(attributes, "gen_ai.agent.description").Should().Be(expectedAgentDetails.AgentDescription); - this.GetAttribute(attributes, "microsoft.agent.user.id").Should().Be(expectedAgentDetails.AgentAUID); - this.GetAttribute(attributes, "microsoft.agent.user.email").Should().Be(expectedAgentDetails.AgentUPN); - this.GetAttribute(attributes, "microsoft.a365.agent.blueprint.id").Should().Be(expectedAgentDetails.AgentBlueprintId); - this.GetAttribute(attributes, "microsoft.tenant.id").Should().Be(tenantDetails.TenantId.ToString()); - this.GetAttribute(attributes, "gen_ai.request.model").Should().Be(inferenceDetails.Model); - this.GetAttribute(attributes, "gen_ai.provider.name").Should().Be(inferenceDetails.ProviderName); - this.GetAttribute(attributes, "gen_ai.usage.input_tokens").Should().Be("42"); - this.GetAttribute(attributes, "gen_ai.usage.output_tokens").Should().Be("84"); - this.GetAttribute(attributes, "gen_ai.response.finish_reasons").Should().Be("stop,length"); - this.GetAttribute(attributes, "gen_ai.input.messages").Should().Be("Hello,World"); - this.GetAttribute(attributes, "gen_ai.output.messages").Should().Be("Hi there!"); - } - - [TestMethod] - public async Task AddTracing_NestedScopes_AllExporterRequestsReceived() - { - // Arrange - List receivedContents = new(); - - var agentType = AgentType.EntraEmbodied; - var agentDetails = new AgentDetails( - agentId: Guid.NewGuid().ToString(), - agentName: "Nested Agent", - agentDescription: "Agent for nested scope testing.", - agentAUID: Guid.NewGuid().ToString(), - agentUPN: "nestedagent@ztaitest12.onmicrosoft.com", - agentBlueprintId: Guid.NewGuid().ToString(), - tenantId: Guid.NewGuid().ToString(), - agentType: agentType); - - var tenantDetails = new TenantDetails(Guid.NewGuid()); - var endpoint = new Uri("https://nested-endpoint"); - - var handler = new TestHttpMessageHandler(req => - { - receivedContents.Add(req.Content?.ReadAsStringAsync().GetAwaiter().GetResult() ?? ""); - return new HttpResponseMessage(System.Net.HttpStatusCode.OK); - }); - var httpClient = new HttpClient(handler); - - this.CreateTestServiceProvider(httpClient); - - var invokeAgentDetails = new InvokeAgentDetails(endpoint: endpoint, details: agentDetails); - var request = new Request( - content: "Nested request", - executionType: ExecutionType.HumanToAgent, - channel: new Channel(name: "nested", link: "https://nestedchannel.link")); - - var toolCallDetails = new ToolCallDetails( - toolName: "NestedTool", - arguments: "{\"param\":\"nested\"}", - toolCallId: "call-nested", - description: "Nested tool call", - toolType: "nested-type", - endpoint: endpoint); - - var inferenceDetails = new InferenceCallDetails( - operationName: InferenceOperationType.Chat, - model: "gpt-nested", - providerName: "OpenAI", - inputTokens: 10, - outputTokens: 20, - finishReasons: new[] { "stop" }, - responseId: "response-nested"); - - // Act - using (var agentScope = InvokeAgentScope.Start(invokeAgentDetails, tenantDetails, request)) - { - agentScope.RecordInputMessages(new[] { "Agent input" }); - agentScope.RecordOutputMessages(new[] { "Agent output" }); - - using (var toolScope = ExecuteToolScope.Start(toolCallDetails, agentDetails, tenantDetails)) - { - toolScope.RecordResponse("Tool response"); - - using (var inferenceScope = InferenceScope.Start(inferenceDetails, agentDetails, tenantDetails)) - { - inferenceScope.RecordInputMessages(new[] { "Inference input" }); - inferenceScope.RecordOutputMessages(new[] { "Inference output" }); - inferenceScope.RecordInputTokens(10); - inferenceScope.RecordOutputTokens(20); - inferenceScope.RecordFinishReasons(new[] { "stop" }); - } - } - } - - // Wait for up to 5 seconds for all spans to be exported - await Task.Delay(5000).ConfigureAwait(false); - - // Assert - var allOperationNames = new List(); - foreach (var content in receivedContents) - { - using var doc = JsonDocument.Parse(content); - var root = doc.RootElement; - var spans = root - .GetProperty("resourceSpans")[0] - .GetProperty("scopeSpans")[0] - .GetProperty("spans") - .EnumerateArray(); - - foreach (var span in spans) - { - var opName = this.GetAttribute(span.GetProperty("attributes"), "gen_ai.operation.name"); - if (opName != null) - allOperationNames.Add(opName); - } - } - allOperationNames.Should().Contain(new[] { "invoke_agent", "execute_tool", InferenceOperationType.Chat.ToString() }, "All three nested scopes should be exported, even if batched in fewer requests."); - } - - private class TestHttpMessageHandler : HttpMessageHandler - { - private Func _handler; - public TestHttpMessageHandler(Func handler) - { - this._handler = handler; - } - public void SetHandler(Func handler) - { - this._handler = handler; - } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return Task.FromResult(this._handler(request)); - } - } - - private string? GetAttribute(JsonElement attributes, string key) - { - if (attributes.TryGetProperty(key, out var value)) - { - if (value.ValueKind == JsonValueKind.String) - { - return value.GetString(); - } - if (value.ValueKind == JsonValueKind.Number) - { - return value.GetRawText(); - } - if (value.ValueKind == JsonValueKind.Object && value.TryGetProperty("stringValue", out var sv)) - { - return sv.GetString(); - } - } - return null; - } - - private ServiceProvider CreateTestServiceProvider(HttpClient httpClient) - { - HostApplicationBuilder builder = new HostApplicationBuilder(); - - builder.Configuration["EnableAgent365Exporter"] = "true"; - builder.Services.AddSingleton(httpClient); - builder.Services.AddSingleton(sp => - { - return new Agent365ExporterOptions - { - UseS2SEndpoint = false, - TokenResolver = (_, _) => Task.FromResult("test-token") - }; - }); - - builder.AddA365Tracing(useOpenTelemetryBuilder: false, agent365ExporterType: Agent365ExporterType.Agent365ExporterAsync); - return builder.Services.BuildServiceProvider(); - } - private void SetupExporterTest() - { - this._handler = new TestHttpMessageHandler(req => - { - this._receivedRequest = true; - this._receivedContent = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); - req.RequestUri.Should().NotBeNull(); - req.Headers.Authorization.Should().NotBeNull(); - return new HttpResponseMessage(System.Net.HttpStatusCode.OK); - }); - var httpClient = new HttpClient(this._handler); - this._provider = this.CreateTestServiceProvider(httpClient); - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FluentAssertions; +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters; +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Net; +using System.Text.Json; + +namespace Microsoft.Agents.A365.Observability.Runtime.Tests.IntegrationTests +{ + [TestClass] + public class Agent365ExporterAsyncE2ETests + { + private TestHttpMessageHandler? _handler; + private ServiceProvider? _provider; + private bool _receivedRequest; + private string? _receivedContent; + + [TestMethod] + public async Task AddTracing_And_InvokeAgentScope_ExporterMakesExpectedRequest() + { + // Arrange + this.SetupExporterTest(); + this._receivedRequest = false; + this._receivedContent = null; + var expectedAgentType = AgentType.EntraEmbodied; + var expectedAgentDetails = new AgentDetails( + agentId: Guid.NewGuid().ToString(), + agentName: "Test Agent", + agentDescription: "Agent for testing.", + agentAUID: Guid.NewGuid().ToString(), + agentUPN: "testagent@ztaitest12.onmicrosoft.com", + agentBlueprintId: Guid.NewGuid().ToString(), + tenantId: Guid.NewGuid().ToString(), + agentType: expectedAgentType); + var endpoint = new Uri("https://test-agent-endpoint"); + var invokeAgentDetails = new InvokeAgentDetails(endpoint: endpoint, details: expectedAgentDetails); + var tenantDetails = new TenantDetails(Guid.NewGuid()); + + var expectedRequest = new Request( + content: "Test request content", + executionType: ExecutionType.HumanToAgent, + channel: new Channel( + name: "msteams", + link: "https://testchannel.link")); + + var expectedCallerDetails = new CallerDetails( + callerId: "caller-123", + callerName: "Test Caller", + callerUpn: "caller-123@ztaitest12.onmicrosoft.com", + callerClientIP: IPAddress.Parse("203.0.113.42"), + tenantId: expectedAgentDetails.TenantId); + + // Act + using (var scope = InvokeAgentScope.Start( + invokeAgentDetails: invokeAgentDetails, + tenantDetails: tenantDetails, + request: expectedRequest, + callerDetails: expectedCallerDetails)) + { + scope.RecordInputMessages(new[] { "Input message 1", "Input message 2" }); + scope.RecordOutputMessages(new[] { "Output message 1" }); + } + + var timeout = TimeSpan.FromSeconds(30); + var start = DateTime.UtcNow; + while (!this._receivedRequest && DateTime.UtcNow - start < timeout) + { + await Task.Delay(1000).ConfigureAwait(false); + } + + this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); + this._receivedContent.Should().NotBeNull("Exporter should send a request body."); + + using var doc = JsonDocument.Parse(this._receivedContent!); + var root = doc.RootElement; + + var attributes = root + .GetProperty("resourceSpans")[0] + .GetProperty("scopeSpans")[0] + .GetProperty("spans")[0] + .GetProperty("attributes"); + this.GetAttribute(attributes, "server.address").Should().Be(invokeAgentDetails.Endpoint?.Host); + this.GetAttribute(attributes, "microsoft.channel.name").Should().Be(expectedRequest.Channel?.Name); + this.GetAttribute(attributes, "microsoft.channel.link").Should().Be(expectedRequest.Channel?.Link); + this.GetAttribute(attributes, "microsoft.tenant.id").Should().Be(tenantDetails.TenantId.ToString()); + this.GetAttribute(attributes, "user.id").Should().Be(expectedCallerDetails.CallerId); + this.GetAttribute(attributes, "user.email").Should().Be(expectedCallerDetails.CallerUpn); + this.GetAttribute(attributes, "user.name").Should().Be(expectedCallerDetails.CallerName); + this.GetAttribute(attributes, "gen_ai.input.messages").Should().Be("Input message 1,Input message 2"); + this.GetAttribute(attributes, "gen_ai.output.messages").Should().Be("Output message 1"); + this.GetAttribute(attributes, "gen_ai.agent.id").Should().Be(expectedAgentDetails.AgentId); + this.GetAttribute(attributes, "gen_ai.agent.name").Should().Be(expectedAgentDetails.AgentName); + this.GetAttribute(attributes, "gen_ai.agent.description").Should().Be(expectedAgentDetails.AgentDescription); + this.GetAttribute(attributes, "microsoft.agent.user.id").Should().Be(expectedAgentDetails.AgentAUID); + this.GetAttribute(attributes, "microsoft.agent.user.email").Should().Be(expectedAgentDetails.AgentUPN); + this.GetAttribute(attributes, "microsoft.a365.agent.blueprint.id").Should().Be(expectedAgentDetails.AgentBlueprintId); + this.GetAttribute(attributes, "microsoft.tenant.id").Should().Be(tenantDetails.TenantId.ToString()); + this.GetAttribute(attributes, "gen_ai.operation.name").Should().Be("invoke_agent"); + } + + [TestMethod] + public async Task AddTracing_And_ExecuteToolScope_ExporterMakesExpectedRequest() + { + // Arrange + this.SetupExporterTest(); + this._receivedRequest = false; + this._receivedContent = null; + var expectedAgentType = AgentType.EntraEmbodied; + var expectedAgentDetails = new AgentDetails( + agentId: Guid.NewGuid().ToString(), + agentName: "Tool Agent", + agentDescription: "Agent for tool execution.", + agentAUID: Guid.NewGuid().ToString(), + agentUPN: "toolagent@ztaitest12.onmicrosoft.com", + agentBlueprintId: Guid.NewGuid().ToString(), + tenantId: Guid.NewGuid().ToString(), + agentType: expectedAgentType); + var tenantDetails = new TenantDetails(Guid.NewGuid()); + var endpoint = new Uri("https://tool-endpoint:8443"); + var toolCallDetails = new ToolCallDetails( + toolName: "TestTool", + arguments: "{\"param\":\"value\"}", + toolCallId: "call-456", + description: "Test tool call description", + toolType: "custom-type", + endpoint: endpoint); + + // Act + using (var scope = ExecuteToolScope.Start(toolCallDetails, expectedAgentDetails, tenantDetails)) + { + scope.RecordResponse("Tool response content"); + } + + var timeout = TimeSpan.FromSeconds(30); + var start = DateTime.UtcNow; + while (!this._receivedRequest && DateTime.UtcNow - start < timeout) + { + await Task.Delay(1000).ConfigureAwait(false); + } + + this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); + this._receivedContent.Should().NotBeNull("Exporter should send a request body."); + + using var doc = JsonDocument.Parse(this._receivedContent!); + var root = doc.RootElement; + + var attributes = root + .GetProperty("resourceSpans")[0] + .GetProperty("scopeSpans")[0] + .GetProperty("spans")[0] + .GetProperty("attributes"); + + this.GetAttribute(attributes, "gen_ai.operation.name").Should().Be("execute_tool"); + this.GetAttribute(attributes, "gen_ai.agent.id").Should().Be(expectedAgentDetails.AgentId); + this.GetAttribute(attributes, "gen_ai.agent.name").Should().Be(expectedAgentDetails.AgentName); + this.GetAttribute(attributes, "gen_ai.agent.description").Should().Be(expectedAgentDetails.AgentDescription); + this.GetAttribute(attributes, "microsoft.agent.user.id").Should().Be(expectedAgentDetails.AgentAUID); + this.GetAttribute(attributes, "microsoft.agent.user.email").Should().Be(expectedAgentDetails.AgentUPN); + this.GetAttribute(attributes, "microsoft.a365.agent.blueprint.id").Should().Be(expectedAgentDetails.AgentBlueprintId); + this.GetAttribute(attributes, "microsoft.tenant.id").Should().Be(tenantDetails.TenantId.ToString()); + this.GetAttribute(attributes, "gen_ai.tool.name").Should().Be(toolCallDetails.ToolName); + this.GetAttribute(attributes, "gen_ai.tool.arguments").Should().Be(toolCallDetails.Arguments); + this.GetAttribute(attributes, "gen_ai.tool.call.id").Should().Be(toolCallDetails.ToolCallId); + this.GetAttribute(attributes, "gen_ai.tool.description").Should().Be(toolCallDetails.Description); + this.GetAttribute(attributes, "gen_ai.tool.type").Should().Be(toolCallDetails.ToolType); + this.GetAttribute(attributes, "server.address").Should().Be(endpoint.Host); + this.GetAttribute(attributes, "server.port").Should().Be(endpoint.Port.ToString()); + this.GetAttribute(attributes, "gen_ai.tool.call.result").Should().Be("Tool response content"); + } + + [TestMethod] + public async Task AddTracing_And_InferenceScope_ExporterMakesExpectedRequest() + { + // Arrange + this.SetupExporterTest(); + this._receivedRequest = false; + this._receivedContent = null; + var expectedAgentType = AgentType.EntraEmbodied; + var expectedAgentDetails = new AgentDetails( + agentId: Guid.NewGuid().ToString(), + agentName: "Inference Agent", + agentDescription: "Agent for inference testing.", + agentAUID: Guid.NewGuid().ToString(), + agentUPN: "inferenceagent@ztaitest12.onmicrosoft.com", + agentBlueprintId: Guid.NewGuid().ToString(), + tenantId: Guid.NewGuid().ToString(), + agentType: expectedAgentType); + var tenantDetails = new TenantDetails(Guid.NewGuid()); + + var inferenceDetails = new InferenceCallDetails( + operationName: InferenceOperationType.Chat, + model: "gpt-4", + providerName: "OpenAI", + inputTokens: 42, + outputTokens: 84, + finishReasons: new[] { "stop", "length" }, + responseId: "response-xyz"); + + // Act + using (var scope = InferenceScope.Start(inferenceDetails, expectedAgentDetails, tenantDetails)) + { + scope.RecordInputMessages(new[] { "Hello", "World" }); + scope.RecordOutputMessages(new[] { "Hi there!" }); + scope.RecordInputTokens(42); + scope.RecordOutputTokens(84); + scope.RecordFinishReasons(new[] { "stop", "length" }); + } + + var timeout = TimeSpan.FromSeconds(30); + var start = DateTime.UtcNow; + while (!this._receivedRequest && DateTime.UtcNow - start < timeout) + { + await Task.Delay(1000).ConfigureAwait(false); + } + + this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); + this._receivedContent.Should().NotBeNull("Exporter should send a request body."); + + using var doc = JsonDocument.Parse(this._receivedContent!); + var root = doc.RootElement; + var attributes = root + .GetProperty("resourceSpans")[0] + .GetProperty("scopeSpans")[0] + .GetProperty("spans")[0] + .GetProperty("attributes"); + + this.GetAttribute(attributes, "gen_ai.operation.name").Should().Be(inferenceDetails.OperationName.ToString()); + this.GetAttribute(attributes, "gen_ai.agent.id").Should().Be(expectedAgentDetails.AgentId); + this.GetAttribute(attributes, "gen_ai.agent.name").Should().Be(expectedAgentDetails.AgentName); + this.GetAttribute(attributes, "gen_ai.agent.description").Should().Be(expectedAgentDetails.AgentDescription); + this.GetAttribute(attributes, "microsoft.agent.user.id").Should().Be(expectedAgentDetails.AgentAUID); + this.GetAttribute(attributes, "microsoft.agent.user.email").Should().Be(expectedAgentDetails.AgentUPN); + this.GetAttribute(attributes, "microsoft.a365.agent.blueprint.id").Should().Be(expectedAgentDetails.AgentBlueprintId); + this.GetAttribute(attributes, "microsoft.tenant.id").Should().Be(tenantDetails.TenantId.ToString()); + this.GetAttribute(attributes, "gen_ai.request.model").Should().Be(inferenceDetails.Model); + this.GetAttribute(attributes, "gen_ai.provider.name").Should().Be(inferenceDetails.ProviderName); + this.GetAttribute(attributes, "gen_ai.usage.input_tokens").Should().Be("42"); + this.GetAttribute(attributes, "gen_ai.usage.output_tokens").Should().Be("84"); + this.GetAttribute(attributes, "gen_ai.response.finish_reasons").Should().Be("stop,length"); + this.GetAttribute(attributes, "gen_ai.input.messages").Should().Be("Hello,World"); + this.GetAttribute(attributes, "gen_ai.output.messages").Should().Be("Hi there!"); + } + + [TestMethod] + public async Task AddTracing_NestedScopes_AllExporterRequestsReceived() + { + // Arrange + List receivedContents = new(); + + var agentType = AgentType.EntraEmbodied; + var agentDetails = new AgentDetails( + agentId: Guid.NewGuid().ToString(), + agentName: "Nested Agent", + agentDescription: "Agent for nested scope testing.", + agentAUID: Guid.NewGuid().ToString(), + agentUPN: "nestedagent@ztaitest12.onmicrosoft.com", + agentBlueprintId: Guid.NewGuid().ToString(), + tenantId: Guid.NewGuid().ToString(), + agentType: agentType); + + var tenantDetails = new TenantDetails(Guid.NewGuid()); + var endpoint = new Uri("https://nested-endpoint"); + + var handler = new TestHttpMessageHandler(req => + { + receivedContents.Add(req.Content?.ReadAsStringAsync().GetAwaiter().GetResult() ?? ""); + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + }); + var httpClient = new HttpClient(handler); + + this.CreateTestServiceProvider(httpClient); + + var invokeAgentDetails = new InvokeAgentDetails(endpoint: endpoint, details: agentDetails); + var request = new Request( + content: "Nested request", + executionType: ExecutionType.HumanToAgent, + channel: new Channel(name: "nested", link: "https://nestedchannel.link")); + + var toolCallDetails = new ToolCallDetails( + toolName: "NestedTool", + arguments: "{\"param\":\"nested\"}", + toolCallId: "call-nested", + description: "Nested tool call", + toolType: "nested-type", + endpoint: endpoint); + + var inferenceDetails = new InferenceCallDetails( + operationName: InferenceOperationType.Chat, + model: "gpt-nested", + providerName: "OpenAI", + inputTokens: 10, + outputTokens: 20, + finishReasons: new[] { "stop" }, + responseId: "response-nested"); + + // Act + using (var agentScope = InvokeAgentScope.Start(invokeAgentDetails, tenantDetails, request)) + { + agentScope.RecordInputMessages(new[] { "Agent input" }); + agentScope.RecordOutputMessages(new[] { "Agent output" }); + + using (var toolScope = ExecuteToolScope.Start(toolCallDetails, agentDetails, tenantDetails)) + { + toolScope.RecordResponse("Tool response"); + + using (var inferenceScope = InferenceScope.Start(inferenceDetails, agentDetails, tenantDetails)) + { + inferenceScope.RecordInputMessages(new[] { "Inference input" }); + inferenceScope.RecordOutputMessages(new[] { "Inference output" }); + inferenceScope.RecordInputTokens(10); + inferenceScope.RecordOutputTokens(20); + inferenceScope.RecordFinishReasons(new[] { "stop" }); + } + } + } + + // Wait for up to 5 seconds for all spans to be exported + await Task.Delay(5000).ConfigureAwait(false); + + // Assert + var allOperationNames = new List(); + foreach (var content in receivedContents) + { + using var doc = JsonDocument.Parse(content); + var root = doc.RootElement; + var spans = root + .GetProperty("resourceSpans")[0] + .GetProperty("scopeSpans")[0] + .GetProperty("spans") + .EnumerateArray(); + + foreach (var span in spans) + { + var opName = this.GetAttribute(span.GetProperty("attributes"), "gen_ai.operation.name"); + if (opName != null) + allOperationNames.Add(opName); + } + } + allOperationNames.Should().Contain(new[] { "invoke_agent", "execute_tool", InferenceOperationType.Chat.ToString() }, "All three nested scopes should be exported, even if batched in fewer requests."); + } + + private class TestHttpMessageHandler : HttpMessageHandler + { + private Func _handler; + public TestHttpMessageHandler(Func handler) + { + this._handler = handler; + } + public void SetHandler(Func handler) + { + this._handler = handler; + } + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(this._handler(request)); + } + } + + private string? GetAttribute(JsonElement attributes, string key) + { + if (attributes.TryGetProperty(key, out var value)) + { + if (value.ValueKind == JsonValueKind.String) + { + return value.GetString(); + } + if (value.ValueKind == JsonValueKind.Number) + { + return value.GetRawText(); + } + if (value.ValueKind == JsonValueKind.Object && value.TryGetProperty("stringValue", out var sv)) + { + return sv.GetString(); + } + } + return null; + } + + private ServiceProvider CreateTestServiceProvider(HttpClient httpClient) + { + HostApplicationBuilder builder = new HostApplicationBuilder(); + + builder.Configuration["EnableAgent365Exporter"] = "true"; + builder.Services.AddSingleton(httpClient); + builder.Services.AddSingleton(sp => + { + return new Agent365ExporterOptions + { + UseS2SEndpoint = false, + TokenResolver = (_, _, _) => Task.FromResult("test-token") + }; + }); + + builder.AddA365Tracing(useOpenTelemetryBuilder: false, agent365ExporterType: Agent365ExporterType.Agent365ExporterAsync); + return builder.Services.BuildServiceProvider(); + } + private void SetupExporterTest() + { + this._handler = new TestHttpMessageHandler(req => + { + this._receivedRequest = true; + this._receivedContent = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + req.RequestUri.Should().NotBeNull(); + req.Headers.Authorization.Should().NotBeNull(); + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + }); + var httpClient = new HttpClient(this._handler); + this._provider = this.CreateTestServiceProvider(httpClient); + } + } +} diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterE2ETests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterE2ETests.cs index 05f152e7..668b0376 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterE2ETests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterE2ETests.cs @@ -543,7 +543,7 @@ public async Task AddTracing_MultipleInvocations_NoDuplicateExports() builder.Services.AddSingleton(_ => new Agent365ExporterOptions { UseS2SEndpoint = false, - TokenResolver = (_, _) => Task.FromResult("test-token") + TokenResolver = (_, _, _) => Task.FromResult("test-token") }); // AddA365Tracing call @@ -636,7 +636,7 @@ private ServiceProvider CreateTestServiceProvider(HttpClient httpClient) return new Agent365ExporterOptions { UseS2SEndpoint = false, - TokenResolver = (_, _) => Task.FromResult("test-token") + TokenResolver = (_, _, _) => Task.FromResult("test-token") }; }); builder.AddA365Tracing(useOpenTelemetryBuilder: false, agent365ExporterType: Agent365ExporterType.Agent365Exporter); diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/BuilderTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/BuilderTests.cs index 8c60b5a3..4f0b5e9e 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/BuilderTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/BuilderTests.cs @@ -71,7 +71,7 @@ public void Builder_WithExporterType_Sync_RegistersTracerProvider() // Provide required dependencies for exporter services.AddSingleton(_ => new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("unit-test-token"), + TokenResolver = (_, _, _) => Task.FromResult("unit-test-token"), UseS2SEndpoint = false }); @@ -106,7 +106,7 @@ public void Builder_WithExporterType_Async_RegistersTracerProvider() // Provide required dependencies for exporter services.AddSingleton(_ => new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("unit-test-token"), + TokenResolver = (_, _, _) => Task.FromResult("unit-test-token"), UseS2SEndpoint = false }); @@ -140,7 +140,7 @@ public void Builder_UseOpenTelemetryBuilder_False_WithExporterType_Sync_CreatesT services.AddSingleton(_ => new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("unit-test-token"), + TokenResolver = (_, _, _) => Task.FromResult("unit-test-token"), UseS2SEndpoint = false }); @@ -177,7 +177,7 @@ public void Builder_UseOpenTelemetryBuilder_False_WithExporterType_Async_Exports services.AddSingleton(_ => new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("unit-test-token"), + TokenResolver = (_, _, _) => Task.FromResult("unit-test-token"), UseS2SEndpoint = false }); diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/Agent365ExporterTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/Agent365ExporterTests.cs index 27af125b..a5b6349d 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/Agent365ExporterTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/Agent365ExporterTests.cs @@ -99,7 +99,7 @@ private static Agent365Exporter CreateExporter(Func? to { var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("token") + TokenResolver = (_, _, _) => Task.FromResult("token") }; var resource = ResourceBuilder.CreateEmpty() @@ -118,7 +118,7 @@ public void Constructor_NullLogger_Throws() { var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("token") + TokenResolver = (_, _, _) => Task.FromResult("token") }; Action act = () => _ = new Agent365Exporter(Agent365ExporterTests._agent365ExporterCore, null!, options, resource: null); act.Should().Throw().WithParameterName("logger"); @@ -196,7 +196,7 @@ public void Agent365ExporterOptions_DefaultBatchingParameters_AreSet() // Arrange & Act var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("token") + TokenResolver = (_, _, _) => Task.FromResult("token") }; // Assert @@ -212,7 +212,7 @@ public void Agent365ExporterOptions_CustomBatchingParameters_CanBeSet() // Arrange & Act var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("token"), + TokenResolver = (_, _, _) => Task.FromResult("token"), MaxQueueSize = 4096, ScheduledDelayMilliseconds = 10000, ExporterTimeoutMilliseconds = 60000, @@ -232,7 +232,7 @@ public void Agent365ExporterOptions_UseS2SEndpoint_DefaultsToFalse() // Arrange & Act var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("token") + TokenResolver = (_, _, _) => Task.FromResult("token") }; // Assert @@ -245,7 +245,7 @@ public void Agent365ExporterOptions_UseS2SEndpoint_CanBeSetToTrue() // Arrange & Act var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("token"), + TokenResolver = (_, _, _) => Task.FromResult("token"), UseS2SEndpoint = true }; @@ -259,7 +259,7 @@ public void Agent365ExporterOptions_UseS2SEndpoint_CanBeSetToFalse() // Arrange & Act var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("token"), + TokenResolver = (_, _, _) => Task.FromResult("token"), UseS2SEndpoint = false }; @@ -275,7 +275,7 @@ public void UseS2SEndpoint_WhenFalse_UsesStandardEndpoint() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -297,7 +297,7 @@ public void UseS2SEndpoint_WhenTrue_UsesS2SEndpoint() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -328,7 +328,7 @@ public void UseS2SEndpoint_CanBeToggled_FromFalseToTrue() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -345,7 +345,7 @@ public void UseS2SEndpoint_CanBeToggled_FromTrueToFalse() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -362,7 +362,7 @@ public void Export_WithMultipleActivities_StandardEndpoint_GroupsByIdentity() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -396,7 +396,7 @@ public void Export_WithMultipleActivities_S2SEndpoint_GroupsByIdentity() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -433,7 +433,7 @@ public void Export_S2SEndpoint_TokenResolverCalled_WithCorrectParameters() var options = new Agent365ExporterOptions { - TokenResolver = (agentId, tenantId) => + TokenResolver = (agentId, tenantId, _) => { capturedAgentId = agentId; capturedTenantId = tenantId; @@ -472,7 +472,7 @@ public void Export_StandardEndpoint_TokenResolverCalled_WithCorrectParameters() var options = new Agent365ExporterOptions { - TokenResolver = (agentId, tenantId) => + TokenResolver = (agentId, tenantId, _) => { capturedAgentId = agentId; capturedTenantId = tenantId; @@ -508,7 +508,7 @@ public void Export_S2SEndpoint_NullToken_StillSendsRequest() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult(null), // Return null token + TokenResolver = (_, _, _) => Task.FromResult(null), // Return null token UseS2SEndpoint = true }; @@ -538,7 +538,7 @@ public void Export_S2SEndpoint_EmptyToken_StillSendsRequest() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult(string.Empty), // Return empty token + TokenResolver = (_, _, _) => Task.FromResult(string.Empty), // Return empty token UseS2SEndpoint = true }; @@ -568,7 +568,7 @@ public void Export_S2SEndpoint_TokenResolverThrows_ReturnsFailure() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromException(new InvalidOperationException("Token resolver failed")), + TokenResolver = (_, _, _) => Task.FromException(new InvalidOperationException("Token resolver failed")), UseS2SEndpoint = true }; @@ -598,7 +598,7 @@ public void Export_StandardEndpoint_TokenResolverThrows_ReturnsFailure() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromException(new InvalidOperationException("Token resolver failed")), + TokenResolver = (_, _, _) => Task.FromException(new InvalidOperationException("Token resolver failed")), UseS2SEndpoint = false }; @@ -628,7 +628,7 @@ public void Export_S2SEndpoint_ActivitiesWithoutIdentity_ReturnsSuccess() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -660,7 +660,7 @@ public void Export_StandardEndpoint_ActivitiesWithoutIdentity_ReturnsSuccess() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -692,7 +692,7 @@ public void Export_S2SEndpoint_ActivityWithOnlyTenantId_IsSkipped() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -722,7 +722,7 @@ public void Export_S2SEndpoint_ActivityWithOnlyAgentId_IsSkipped() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -752,7 +752,7 @@ public void Export_StandardEndpoint_ActivityWithOnlyTenantId_IsSkipped() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -782,7 +782,7 @@ public void Export_StandardEndpoint_ActivityWithOnlyAgentId_IsSkipped() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -812,7 +812,7 @@ public void Export_S2SEndpoint_MixedBatch_ProcessesOnlyValidActivities() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -846,7 +846,7 @@ public void Export_StandardEndpoint_MixedBatch_ProcessesOnlyValidActivities() // Arrange var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -889,7 +889,7 @@ public void Export_S2SEndpoint_WithCustomResource_ProcessesCorrectly() var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -924,7 +924,7 @@ public void Export_StandardEndpoint_WithCustomResource_ProcessesCorrectly() var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -954,7 +954,7 @@ public void Export_S2SEndpoint_WithDifferentClusterCategories_ProcessesCorrectly var options = new Agent365ExporterOptions { ClusterCategory = category, - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = true }; @@ -989,7 +989,7 @@ public void Export_StandardEndpoint_WithDifferentClusterCategories_ProcessesCorr var options = new Agent365ExporterOptions { ClusterCategory = category, - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; @@ -1036,7 +1036,7 @@ public void Export_RequestUri_EnvVar_Overrides_CustomDomainResolver_WhenBothSet( var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false, DomainResolver = tenantId => resolverDomain }; @@ -1086,7 +1086,7 @@ public void Export_RequestUri_UsesEnvVar_WhenNoResolverSet() var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false, }; @@ -1136,7 +1136,7 @@ public void Export_RequestUri_UsesCustomDomainResolver_WhenProvided() var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false, DomainResolver = tenantId => resolverDomain }; @@ -1184,7 +1184,7 @@ public void Export_RequestUri_UsesDefaultEndpoint_WhenNoResolverAndNoEnvVarSet() var options = new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), UseS2SEndpoint = false }; diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/ObservabilityBuilderExtensionsTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/ObservabilityBuilderExtensionsTests.cs index 67ca662d..19c26a67 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/ObservabilityBuilderExtensionsTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/ObservabilityBuilderExtensionsTests.cs @@ -19,7 +19,7 @@ public void AddAgent365Exporter_WithCustomBatchingParameters_ShouldConfigureProc // Configure custom batching parameters services.AddSingleton(new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token"), + TokenResolver = (_, _, _) => Task.FromResult("test-token"), MaxQueueSize = 4096, ScheduledDelayMilliseconds = 10000, ExporterTimeoutMilliseconds = 60000, @@ -55,7 +55,7 @@ public void AddAgent365Exporter_WithDefaultBatchingParameters_ShouldUseDefaults( // Configure with default batching parameters services.AddSingleton(new Agent365ExporterOptions { - TokenResolver = (_, _) => Task.FromResult("test-token") + TokenResolver = (_, _, _) => Task.FromResult("test-token") }); var tracerProviderBuilder = services.AddOpenTelemetry().WithTracing(builder => diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/McpToolServerConfigurationService_ToolEnumerationTests.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/McpToolServerConfigurationService_ToolEnumerationTests.cs index 90d6eef8..1b7ddddb 100644 --- a/src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/McpToolServerConfigurationService_ToolEnumerationTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/McpToolServerConfigurationService_ToolEnumerationTests.cs @@ -54,7 +54,7 @@ public async Task EnumerateToolsFromServersAsync_WhenListServersFails_ReturnsEmp // Arrange var toolOptions = new ToolOptions(); _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception("Network error")); // Act @@ -75,7 +75,7 @@ public async Task EnumerateToolsFromServersAsync_WhenNoServersConfigured_Returns // Arrange var toolOptions = new ToolOptions(); _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); // Act @@ -102,7 +102,7 @@ public async Task EnumerateToolsFromServersAsync_FiltersInvalidServers_WithMissi }; _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(servers); _mockService @@ -139,7 +139,7 @@ public async Task EnumerateToolsFromServersAsync_FiltersInvalidServers_WithMissi }; _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(servers); _mockService @@ -178,7 +178,7 @@ public async Task EnumerateToolsFromServersAsync_EnumeratesToolsFromMultipleServ var tools2 = new List(); _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(servers); _mockService @@ -228,7 +228,7 @@ public async Task EnumerateToolsFromServersAsync_HandlesIndividualServerFailures var workingTools = new List(); _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(servers); _mockService @@ -281,7 +281,7 @@ public async Task EnumerateToolsFromServersAsync_EnumeratesInParallel() var tcs3 = new TaskCompletionSource>(); _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(servers); _mockService @@ -382,7 +382,7 @@ public async Task EnumerateAllToolsAsync_ReturnsFlatListOfAllTools() var tools2 = new List(); _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(servers); _mockService @@ -421,7 +421,7 @@ public async Task EnumerateAllToolsAsync_WhenNoServers_ReturnsEmptyList() // Arrange var toolOptions = new ToolOptions(); _mockService - .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.ListToolServersAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); // Act diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/AddToolServersToAgent_Tests.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/AddToolServersToAgent_Tests.cs index 9dac3ae8..8646776f 100644 --- a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/AddToolServersToAgent_Tests.cs +++ b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/AddToolServersToAgent_Tests.cs @@ -65,7 +65,8 @@ await service.AddToolServersToAgent( TestAgentUserId, TestAuthToken, mockTurnContext.Object, - It.Is(o => o.UserAgentConfiguration == Agent365AgentFrameworkSdkUserAgentConfiguration.Instance)), + It.Is(o => o.UserAgentConfiguration == Agent365AgentFrameworkSdkUserAgentConfiguration.Instance), + It.IsAny()), Times.Once); } diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/GetMcpToolsAsync_Tests.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/GetMcpToolsAsync_Tests.cs index 874131c1..9f0e5c53 100644 --- a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/GetMcpToolsAsync_Tests.cs +++ b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Tests/Services/McpToolRegistrationServiceTests/GetMcpToolsAsync_Tests.cs @@ -37,7 +37,8 @@ await service.GetMcpToolsAsync( TestAgentUserId, TestAuthToken, mockTurnContext.Object, - It.Is(o => o.UserAgentConfiguration == Agent365AgentFrameworkSdkUserAgentConfiguration.Instance)), + It.Is(o => o.UserAgentConfiguration == Agent365AgentFrameworkSdkUserAgentConfiguration.Instance), + It.IsAny()), Times.Once); } diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AzureAIFoundry.Tests/McpToolRegistrationServiceTests.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AzureAIFoundry.Tests/McpToolRegistrationServiceTests.cs index d3ef8ef6..3c7d65cb 100644 --- a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AzureAIFoundry.Tests/McpToolRegistrationServiceTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.AzureAIFoundry.Tests/McpToolRegistrationServiceTests.cs @@ -94,11 +94,12 @@ private void SetupMocksForEmptyToolEnumeration(Action? captureToolO It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())); + It.IsAny(), + It.IsAny())); if (captureToolOptions != null) { - setup.Callback((_, _, _, options) => captureToolOptions(options)); + setup.Callback((_, _, _, options, _) => captureToolOptions(options)); } setup.ReturnsAsync((new List(), new Dictionary>())); @@ -114,7 +115,8 @@ private void SetupMocksForToolEnumeration(List servers, Diction It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .ReturnsAsync((servers, toolsByServer)); } @@ -197,7 +199,8 @@ await _service.GetMcpToolDefinitionsAndResourcesAsync( TestAgentInstanceId, TestAuthToken, _mockTurnContext.Object, - It.Is(o => o.UserAgentConfiguration == Agent365AzureAIFoundrySdkUserAgentConfiguration.Instance)), + It.Is(o => o.UserAgentConfiguration == Agent365AzureAIFoundrySdkUserAgentConfiguration.Instance), + It.IsAny()), Times.Once); } diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Tests/Services/McpToolRegistrationServiceTests.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Tests/Services/McpToolRegistrationServiceTests.cs index 5a8e29ea..610cbfcb 100644 --- a/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Tests/Services/McpToolRegistrationServiceTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Tests/Services/McpToolRegistrationServiceTests.cs @@ -111,11 +111,12 @@ private void SetupMocksForEmptyToolEnumeration(Action? captureToolO It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())); + It.IsAny(), + It.IsAny())); if (captureToolOptions != null) { - setup.Callback((_, _, _, options) => captureToolOptions(options)); + setup.Callback((_, _, _, options, _) => captureToolOptions(options)); } setup.ReturnsAsync((new List(), new Dictionary>())); @@ -168,7 +169,8 @@ await service.AddToolServersToAgentAsync( It.IsAny(), _testJwtToken, mockTurnContext.Object, - It.Is(o => o.UserAgentConfiguration == Agent365SemanticKernelSdkUserAgentConfiguration.Instance)), + It.Is(o => o.UserAgentConfiguration == Agent365SemanticKernelSdkUserAgentConfiguration.Instance), + It.IsAny()), Times.Once); } diff --git a/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs b/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs index 664e48b4..7edf7e26 100644 --- a/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs +++ b/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs @@ -19,8 +19,9 @@ public interface IMcpToolServerConfigurationService /// /// Agent instance Id for the agent. /// Auth token to access the MCP servers + /// A cancellation token to cancel the operation. /// Returns the list of MCP Servers that are configured. - Task> ListToolServersAsync(string agentInstanceId, string authToken); + Task> ListToolServersAsync(string agentInstanceId, string authToken, CancellationToken cancellationToken = default); /// /// Gets the list of MCP Servers that are configured for the agent. @@ -28,8 +29,9 @@ public interface IMcpToolServerConfigurationService /// Agent instance Id for the agent. /// Auth token to access the MCP servers /// Tool options for listing servers. + /// A cancellation token to cancel the operation. /// Returns the list of MCP Servers that are configured. - Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions); + Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken = default); /// /// Gets the MCP Client Tools from the specified MCP server. diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs index 68020ef2..f0c9092f 100644 --- a/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs +++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs @@ -33,7 +33,8 @@ public partial class McpToolServerConfigurationService servers = await ListToolServersAsync( agentInstanceId, authToken, - toolOptions).ConfigureAwait(false); + toolOptions, + cancellationToken).ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs index 1f39fc97..5fefb87c 100644 --- a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs +++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs @@ -11,6 +11,7 @@ namespace Microsoft.Agents.A365.Tooling.Services using System.Reflection; using System.Text; using System.Text.Json; + using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.A365.Runtime; using Microsoft.Agents.A365.Tooling.Handlers; @@ -49,15 +50,15 @@ public McpToolServerConfigurationService(ILogger - public virtual async Task> ListToolServersAsync(string agentInstanceId, string authToken) + public virtual async Task> ListToolServersAsync(string agentInstanceId, string authToken, CancellationToken cancellationToken = default) { - return await ListToolServersAsync(agentInstanceId, authToken, new ToolOptions()); + return await ListToolServersAsync(agentInstanceId, authToken, new ToolOptions(), cancellationToken); } /// - public virtual async Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions) + public virtual async Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken = default) { - return IsDevScenario() ? GetMCPServersFromManifest() : await GetMCPServerFromToolingGatewayAsync(agentInstanceId, authToken, toolOptions); + return IsDevScenario() ? GetMCPServersFromManifest() : await GetMCPServerFromToolingGatewayAsync(agentInstanceId, authToken, toolOptions, cancellationToken); } /// @@ -158,7 +159,7 @@ public async Task SendChatHistoryAsync(ITurnContext turnContext } private async Task> GetMCPServerFromToolingGatewayAsync( - string agentInstanceId, string authToken, ToolOptions toolOptions) + string agentInstanceId, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken = default) { string configEndpoint = Utility.GetToolingGatewayForDigitalWorker(agentInstanceId, this._configuration); @@ -174,7 +175,7 @@ private async Task> GetMCPServerFromToolingGatewayAsync( httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken); - var response = await httpClient.GetStringAsync(configEndpoint); + var response = await httpClient.GetStringAsync(configEndpoint, cancellationToken); var options = new JsonSerializerOptions { From 37f38c0ac9c57f0b950e4b3c6351bd341e808892 Mon Sep 17 00:00:00 2001 From: Rido Date: Tue, 24 Mar 2026 22:35:56 -0700 Subject: [PATCH 3/8] Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Agent365ExporterAsyncE2ETests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs index 125675dc..1b770a01 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs @@ -76,7 +76,8 @@ public async Task AddTracing_And_InvokeAgentScope_ExporterMakesExpectedRequest() this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); this._receivedContent.Should().NotBeNull("Exporter should send a request body."); - using var doc = JsonDocument.Parse(this._receivedContent!); + var content = this._receivedContent!; + using var doc = JsonDocument.Parse(content); var root = doc.RootElement; var attributes = root From c6809649a69474fcfbdfd1ce4872d304d39de430 Mon Sep 17 00:00:00 2001 From: Rido Date: Tue, 24 Mar 2026 22:36:15 -0700 Subject: [PATCH 4/8] Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Agent365ExporterAsyncE2ETests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs index 1b770a01..2941c63c 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs @@ -145,9 +145,9 @@ public async Task AddTracing_And_ExecuteToolScope_ExporterMakesExpectedRequest() } this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); - this._receivedContent.Should().NotBeNull("Exporter should send a request body."); + var receivedContent = this._receivedContent.Should().NotBeNull("Exporter should send a request body.").Subject; - using var doc = JsonDocument.Parse(this._receivedContent!); + using var doc = JsonDocument.Parse(receivedContent); var root = doc.RootElement; var attributes = root From 6911428b664cbaacaf4550f341e525bbd340a9ad Mon Sep 17 00:00:00 2001 From: Rido Date: Tue, 24 Mar 2026 22:36:28 -0700 Subject: [PATCH 5/8] Potential fix for pull request finding 'Missing Dispose call on local IDisposable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Agent365ExporterAsyncE2ETests.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs index 2941c63c..cd75e75b 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs @@ -15,6 +15,7 @@ namespace Microsoft.Agents.A365.Observability.Runtime.Tests.IntegrationTests [TestClass] public class Agent365ExporterAsyncE2ETests { + private HttpClient? _httpClient; private TestHttpMessageHandler? _handler; private ServiceProvider? _provider; private bool _receivedRequest; @@ -410,8 +411,15 @@ private void SetupExporterTest() req.Headers.Authorization.Should().NotBeNull(); return new HttpResponseMessage(System.Net.HttpStatusCode.OK); }); - var httpClient = new HttpClient(this._handler); - this._provider = this.CreateTestServiceProvider(httpClient); + this._httpClient = new HttpClient(this._handler); + this._provider = this.CreateTestServiceProvider(this._httpClient); + } + + [TestCleanup] + public void Cleanup() + { + this._httpClient?.Dispose(); + this._httpClient = null; } } } From be258703ec758f4228e9951eca6fa1261469df2b Mon Sep 17 00:00:00 2001 From: Rido Date: Tue, 24 Mar 2026 22:36:50 -0700 Subject: [PATCH 6/8] Potential fix for pull request finding 'Missed opportunity to use Select' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Agent365ExporterAsyncE2ETests.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs index cd75e75b..5def8058 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs @@ -336,12 +336,10 @@ public async Task AddTracing_NestedScopes_AllExporterRequestsReceived() .GetProperty("spans") .EnumerateArray(); - foreach (var span in spans) - { - var opName = this.GetAttribute(span.GetProperty("attributes"), "gen_ai.operation.name"); - if (opName != null) - allOperationNames.Add(opName); - } + allOperationNames.AddRange( + spans + .Select(span => this.GetAttribute(span.GetProperty("attributes"), "gen_ai.operation.name")) + .Where(opName => opName != null)!); } allOperationNames.Should().Contain(new[] { "invoke_agent", "execute_tool", InferenceOperationType.Chat.ToString() }, "All three nested scopes should be exported, even if batched in fewer requests."); } From 9802b825dcd2e50a2a9c80d83d088e91323abaa5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:41:57 +0000 Subject: [PATCH 7/8] fix: revert .Subject anti-pattern introduced by Copilot Autofix that broke CI build Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/Agent365-dotnet/sessions/7825aa1c-1b48-439e-9e3b-2500cba9fd81 --- .../Agent365ExporterAsyncE2ETests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs index 5def8058..a435b3ae 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs @@ -146,9 +146,9 @@ public async Task AddTracing_And_ExecuteToolScope_ExporterMakesExpectedRequest() } this._receivedRequest.Should().BeTrue("Exporter should make the expected HTTP request."); - var receivedContent = this._receivedContent.Should().NotBeNull("Exporter should send a request body.").Subject; + this._receivedContent.Should().NotBeNull("Exporter should send a request body."); - using var doc = JsonDocument.Parse(receivedContent); + using var doc = JsonDocument.Parse(this._receivedContent!); var root = doc.RootElement; var attributes = root From ef5e19115f66802bfdc59d11dd9dddb2c03c9c72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:34:30 +0000 Subject: [PATCH 8/8] fix: apply all PR reviewer suggestions - backward-compat overloads, TestCleanup disposal, XML doc fix Agent-Logs-Url: https://github.com/microsoft/Agent365-dotnet/sessions/bf4ab31c-8d71-4206-bb8c-d96357a4d305 Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> --- .../Hosting/Caching/AgenticTokenCache.cs | 7 ++- .../Hosting/Caching/IExporterTokenCache.cs | 7 ++- .../Hosting/Caching/ServiceTokenCache.cs | 7 ++- .../Agent365ExporterAsyncE2ETests.cs | 2 + .../IMcpToolServerConfigurationService.cs | 58 +++++++++++++++++-- ...verConfigurationService.ToolEnumeration.cs | 20 ++++++- .../McpToolServerConfigurationService.cs | 22 ++++++- .../Services/IMcpToolRegistrationService.cs | 50 ++++++++++++++-- .../Services/McpToolRegistrationService.cs | 29 ++++++++-- .../Services/IMcpToolRegistrationService.cs | 34 ++++++++++- .../Services/McpToolRegistrationService.cs | 22 ++++++- .../Services/IMcpToolRegistrationService.cs | 16 ++++- .../Services/McpToolRegistrationService.cs | 6 +- 13 files changed, 248 insertions(+), 32 deletions(-) diff --git a/src/Observability/Hosting/Caching/AgenticTokenCache.cs b/src/Observability/Hosting/Caching/AgenticTokenCache.cs index cf086e86..86fe99d5 100644 --- a/src/Observability/Hosting/Caching/AgenticTokenCache.cs +++ b/src/Observability/Hosting/Caching/AgenticTokenCache.cs @@ -93,11 +93,14 @@ public void RegisterObservability(string agentId, string tenantId, AgenticTokenS /// /// The agent identifier. /// The tenant identifier. - /// A cancellation token to cancel the operation. /// /// The observability token if available; otherwise, null. /// - public async Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken = default) + public Task GetObservabilityToken(string agentId, string tenantId) + => GetObservabilityToken(agentId, tenantId, CancellationToken.None); + + /// + public async Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken) { if (!_map.TryGetValue($"{agentId}:{tenantId}", out var entry)) return null; diff --git a/src/Observability/Hosting/Caching/IExporterTokenCache.cs b/src/Observability/Hosting/Caching/IExporterTokenCache.cs index b4d60c48..f0e45eca 100644 --- a/src/Observability/Hosting/Caching/IExporterTokenCache.cs +++ b/src/Observability/Hosting/Caching/IExporterTokenCache.cs @@ -18,6 +18,11 @@ public interface IExporterTokenCache where T : class /// /// Returns an observability token (cached inside the credential) or null on failure/not registered. /// - Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken = default); + Task GetObservabilityToken(string agentId, string tenantId); + + /// + /// Returns an observability token (cached inside the credential) or null on failure/not registered, with cancellation support. + /// + Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Observability/Hosting/Caching/ServiceTokenCache.cs b/src/Observability/Hosting/Caching/ServiceTokenCache.cs index 76f7c7fd..07424c86 100644 --- a/src/Observability/Hosting/Caching/ServiceTokenCache.cs +++ b/src/Observability/Hosting/Caching/ServiceTokenCache.cs @@ -121,9 +121,12 @@ public void RegisterObservability(string agentId, string tenantId, string token, /// /// The agent identifier. /// The tenant identifier. - /// A cancellation token to cancel the operation. /// The observability token if valid; otherwise, null. - public Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken = default) + public Task GetObservabilityToken(string agentId, string tenantId) + => GetObservabilityToken(agentId, tenantId, CancellationToken.None); + + /// + public Task GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(agentId) || string.IsNullOrWhiteSpace(tenantId)) return Task.FromResult(null); diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs index a435b3ae..07b2685a 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.IntegrationTests/Agent365ExporterAsyncE2ETests.cs @@ -416,6 +416,8 @@ private void SetupExporterTest() [TestCleanup] public void Cleanup() { + (this._provider as IDisposable)?.Dispose(); + this._provider = null; this._httpClient?.Dispose(); this._httpClient = null; } diff --git a/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs b/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs index 7edf7e26..99f662e7 100644 --- a/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs +++ b/src/Tooling/Core/Services/IMcpToolServerConfigurationService.cs @@ -14,6 +14,14 @@ namespace Microsoft.Agents.A365.Tooling.Services /// public interface IMcpToolServerConfigurationService { + /// + /// Gets the list of MCP Servers that are configured for the agent. + /// + /// Agent instance Id for the agent. + /// Auth token to access the MCP servers + /// Returns the list of MCP Servers that are configured. + Task> ListToolServersAsync(string agentInstanceId, string authToken); + /// /// Gets the list of MCP Servers that are configured for the agent. /// @@ -21,7 +29,16 @@ public interface IMcpToolServerConfigurationService /// Auth token to access the MCP servers /// A cancellation token to cancel the operation. /// Returns the list of MCP Servers that are configured. - Task> ListToolServersAsync(string agentInstanceId, string authToken, CancellationToken cancellationToken = default); + Task> ListToolServersAsync(string agentInstanceId, string authToken, CancellationToken cancellationToken); + + /// + /// Gets the list of MCP Servers that are configured for the agent. + /// + /// Agent instance Id for the agent. + /// Auth token to access the MCP servers + /// Tool options for listing servers. + /// Returns the list of MCP Servers that are configured. + Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions); /// /// Gets the list of MCP Servers that are configured for the agent. @@ -31,7 +48,18 @@ public interface IMcpToolServerConfigurationService /// Tool options for listing servers. /// A cancellation token to cancel the operation. /// Returns the list of MCP Servers that are configured. - Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken = default); + Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken); + + /// + /// Gets the MCP Client Tools from the specified MCP server. + /// + /// The turn context. + /// The MCP server configuration. + /// The authentication token. + /// Tool options for listing servers. + /// MCP Client Tools + /// + Task> GetMcpClientToolsAsync(ITurnContext turnContext, MCPServerConfig mCPServerConfig, string authToken, ToolOptions toolOptions); /// /// Gets the MCP Client Tools from the specified MCP server. @@ -43,7 +71,7 @@ public interface IMcpToolServerConfigurationService /// A cancellation token to cancel the operation. /// MCP Client Tools /// - Task> GetMcpClientToolsAsync(ITurnContext turnContext, MCPServerConfig mCPServerConfig, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken = default); + Task> GetMcpClientToolsAsync(ITurnContext turnContext, MCPServerConfig mCPServerConfig, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken); /// /// Sends chat history to the MCP platform for real-time threat protection. @@ -78,6 +106,16 @@ public interface IMcpToolServerConfigurationService /// Task SendChatHistoryAsync(ITurnContext turnContext, ChatHistoryMessage[] chatHistoryMessages, ToolOptions toolOptions, CancellationToken cancellationToken = default); + /// + /// Enumerates all MCP tools from configured servers for a given agent. + /// + /// The agent instance ID. + /// Authentication token for MCP server access. + /// Turn context for the current request. + /// Tool options including user agent configuration. + /// A tuple containing server configurations and a dictionary mapping server names to their available tools. + Task<(List Servers, Dictionary> ToolsByServer)> EnumerateToolsFromServersAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions); + /// /// Enumerates all MCP tools from configured servers for a given agent. /// @@ -87,7 +125,17 @@ public interface IMcpToolServerConfigurationService /// Tool options including user agent configuration. /// A cancellation token to cancel the operation. /// A tuple containing server configurations and a dictionary mapping server names to their available tools. - Task<(List Servers, Dictionary> ToolsByServer)> EnumerateToolsFromServersAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions, CancellationToken cancellationToken = default); + Task<(List Servers, Dictionary> ToolsByServer)> EnumerateToolsFromServersAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions, CancellationToken cancellationToken); + + /// + /// Enumerates all MCP tools from configured servers, returning a flat list of all tools. + /// + /// The agent instance ID. + /// Authentication token for MCP server access. + /// Turn context for the current request. + /// Tool options including user agent configuration. + /// A flat list of all MCP tools from all configured servers. + Task> EnumerateAllToolsAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions); /// /// Enumerates all MCP tools from configured servers, returning a flat list of all tools. @@ -98,6 +146,6 @@ public interface IMcpToolServerConfigurationService /// Tool options including user agent configuration. /// A cancellation token to cancel the operation. /// A flat list of all MCP tools from all configured servers. - Task> EnumerateAllToolsAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions, CancellationToken cancellationToken = default); + Task> EnumerateAllToolsAsync(string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions, CancellationToken cancellationToken); } } diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs index f0c9092f..deee4b7b 100644 --- a/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs +++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.ToolEnumeration.cs @@ -17,13 +17,21 @@ namespace Microsoft.Agents.A365.Tooling.Services /// public partial class McpToolServerConfigurationService { + /// + public virtual Task<(List Servers, Dictionary> ToolsByServer)> EnumerateToolsFromServersAsync( + string agentInstanceId, + string authToken, + ITurnContext turnContext, + ToolOptions toolOptions) + => EnumerateToolsFromServersAsync(agentInstanceId, authToken, turnContext, toolOptions, CancellationToken.None); + /// public virtual async Task<(List Servers, Dictionary> ToolsByServer)> EnumerateToolsFromServersAsync( string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken) { var toolsByServer = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -110,13 +118,21 @@ public partial class McpToolServerConfigurationService return (servers, toolsByServer); } + /// + public virtual Task> EnumerateAllToolsAsync( + string agentInstanceId, + string authToken, + ITurnContext turnContext, + ToolOptions toolOptions) + => EnumerateAllToolsAsync(agentInstanceId, authToken, turnContext, toolOptions, CancellationToken.None); + /// public virtual async Task> EnumerateAllToolsAsync( string agentInstanceId, string authToken, ITurnContext turnContext, ToolOptions toolOptions, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken) { var (_, toolsByServer) = await EnumerateToolsFromServersAsync( agentInstanceId, diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs index 5fefb87c..0ff7d7a3 100644 --- a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs +++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs @@ -50,24 +50,40 @@ public McpToolServerConfigurationService(ILogger - public virtual async Task> ListToolServersAsync(string agentInstanceId, string authToken, CancellationToken cancellationToken = default) + public virtual Task> ListToolServersAsync(string agentInstanceId, string authToken) + => ListToolServersAsync(agentInstanceId, authToken, CancellationToken.None); + + /// + public virtual async Task> ListToolServersAsync(string agentInstanceId, string authToken, CancellationToken cancellationToken) { return await ListToolServersAsync(agentInstanceId, authToken, new ToolOptions(), cancellationToken); } /// - public virtual async Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken = default) + public virtual Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions) + => ListToolServersAsync(agentInstanceId, authToken, toolOptions, CancellationToken.None); + + /// + public virtual async Task> ListToolServersAsync(string agentInstanceId, string authToken, ToolOptions toolOptions, CancellationToken cancellationToken) { return IsDevScenario() ? GetMCPServersFromManifest() : await GetMCPServerFromToolingGatewayAsync(agentInstanceId, authToken, toolOptions, cancellationToken); } + /// + public virtual Task> GetMcpClientToolsAsync( + ITurnContext turnContext, + MCPServerConfig mCPServerConfig, + string authToken, + ToolOptions toolOptions) + => GetMcpClientToolsAsync(turnContext, mCPServerConfig, authToken, toolOptions, CancellationToken.None); + /// public virtual async Task> GetMcpClientToolsAsync( ITurnContext turnContext, MCPServerConfig mCPServerConfig, string authToken, ToolOptions toolOptions, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken) { try { diff --git a/src/Tooling/Extensions/AgentFramework/Services/IMcpToolRegistrationService.cs b/src/Tooling/Extensions/AgentFramework/Services/IMcpToolRegistrationService.cs index 454e88b9..cb8c6d33 100644 --- a/src/Tooling/Extensions/AgentFramework/Services/IMcpToolRegistrationService.cs +++ b/src/Tooling/Extensions/AgentFramework/Services/IMcpToolRegistrationService.cs @@ -18,6 +18,32 @@ namespace Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services; /// public interface IMcpToolRegistrationService { + /// + /// Add new MCP servers to the agent by creating a new Agent instance. + /// + /// Note: Due to Microsoft.Extensions.AI framework limitations, MCP tools must be set during + /// Agent creation. If new tools are found, this method creates a new Agent + /// instance with all tools (existing + new) properly initialized. + /// + /// The configured IChatClient to use for creating the agent. + /// The agent instructions. + /// The existing tools to keep and add MCP tools to. + /// Agent User Id for the agent. + /// Turn context for the current request + /// User authorization information + /// Authentication Handler Name for use with the UserAuthorization System + /// Optional auth token to access the MCP servers. + /// New Agent instance with all MCP tools, or agent with original tools if no new servers + Task AddToolServersToAgent( + IChatClient chatClient, + string agentInstructions, + IList initialTools, + string agentUserId, + UserAuthorization userAuthorization, + string authHandlerName, + ITurnContext turnContext, + string? authToken = null); + /// /// Add new MCP servers to the agent by creating a new Agent instance. /// @@ -43,8 +69,24 @@ Task AddToolServersToAgent( UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null, - CancellationToken cancellationToken = default); + string? authToken, + CancellationToken cancellationToken); + + /// + /// Returns a List of MCP tools to be added to the agent. + /// + /// Agent User Id for the agent. + /// Turn context for the current request + /// User authorization information + /// Authentication Handler Name for use with the UserAuthorization System + /// Optional auth token to access the MCP servers. + /// List of AI Tools be added to an agent. + Task> GetMcpToolsAsync( + string agentUserId, + UserAuthorization userAuthorization, + string authHandlerName, + ITurnContext turnContext, + string? authToken = null); /// /// Returns a List of MCP tools to be added to the agent. @@ -61,8 +103,8 @@ Task> GetMcpToolsAsync( UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null, - CancellationToken cancellationToken = default); + string? authToken, + CancellationToken cancellationToken); /// /// Sends chat history to the MCP platform. diff --git a/src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs b/src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs index 5eefacab..0390ad35 100644 --- a/src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs +++ b/src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs @@ -44,6 +44,18 @@ public McpToolRegistrationService( _configuration = configuration; } + /// + public Task AddToolServersToAgent( + IChatClient chatClient, + string agentInstructions, + IList initialTools, + string agentUserId, + UserAuthorization userAuthorization, + string authHandlerName, + ITurnContext turnContext, + string? authToken = null) + => AddToolServersToAgent(chatClient, agentInstructions, initialTools, agentUserId, userAuthorization, authHandlerName, turnContext, authToken, CancellationToken.None); + /// public async Task AddToolServersToAgent( IChatClient chatClient, @@ -53,8 +65,8 @@ public async Task AddToolServersToAgent( UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null, - CancellationToken cancellationToken = default) + string? authToken, + CancellationToken cancellationToken) { if (chatClient == null) { @@ -112,14 +124,23 @@ public async Task AddToolServersToAgent( } } + /// + public Task> GetMcpToolsAsync( + string agentUserId, + UserAuthorization userAuthorization, + string authHandlerName, + ITurnContext turnContext, + string? authToken = null) + => GetMcpToolsAsync(agentUserId, userAuthorization, authHandlerName, turnContext, authToken, CancellationToken.None); + /// public async Task> GetMcpToolsAsync( string agentUserId, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null, - CancellationToken cancellationToken = default) + string? authToken, + CancellationToken cancellationToken) { try { diff --git a/src/Tooling/Extensions/AzureAIFoundry/Services/IMcpToolRegistrationService.cs b/src/Tooling/Extensions/AzureAIFoundry/Services/IMcpToolRegistrationService.cs index e30d2cff..b26f77b6 100644 --- a/src/Tooling/Extensions/AzureAIFoundry/Services/IMcpToolRegistrationService.cs +++ b/src/Tooling/Extensions/AzureAIFoundry/Services/IMcpToolRegistrationService.cs @@ -17,6 +17,22 @@ namespace Microsoft.Agents.A365.Tooling.Extensions.AzureFoundry.Services; /// public interface IMcpToolRegistrationService { + /// + /// Loads/initializes configured MCP tool servers for the specified agent with full context. + /// This is the primary method that customers should use in orchestrators with full authentication context. + /// + /// The PersistentAgentsClient instance. + /// User authorization context. + /// Authentication Handler Name for use with the UserAuthorization System + /// Turn context for the conversation. + /// Optional auth token to access the MCP servers. + Task AddToolServersToAgentAsync( + PersistentAgentsClient agentClient, + UserAuthorization userAuthorization, + string authHandlerName, + ITurnContext turnContext, + string? authToken = null); + /// /// Loads/initializes configured MCP tool servers for the specified agent with full context. /// This is the primary method that customers should use in orchestrators with full authentication context. @@ -32,8 +48,20 @@ Task AddToolServersToAgentAsync( UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null, - CancellationToken cancellationToken = default); + string? authToken, + CancellationToken cancellationToken); + + /// + /// Get MCP tool definitions and resources asynchronously. + /// + /// Agent Instance Id for the agent. + /// Auth token to access the MCP servers. + /// Turn context for the conversation. + /// A tuple containing the list of MCP tool definitions and tool resources. + Task<(IList ToolDefinitions, ToolResources? ToolResources)> GetMcpToolDefinitionsAndResourcesAsync( + string agentInstanceId, + string authToken, + ITurnContext turnContext); /// /// Get MCP tool definitions and resources asynchronously. @@ -47,7 +75,7 @@ Task AddToolServersToAgentAsync( string agentInstanceId, string authToken, ITurnContext turnContext, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken); /// /// Sends chat history to the MCP platform for real-time threat protection. diff --git a/src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs b/src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs index d0d62499..d0bbacd3 100644 --- a/src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs +++ b/src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs @@ -91,14 +91,23 @@ public void AddToolServersToAgent( } } + /// + public Task AddToolServersToAgentAsync( + PersistentAgentsClient agentClient, + UserAuthorization userAuthorization, + string authHandlerName, + ITurnContext turnContext, + string? authToken = null) + => AddToolServersToAgentAsync(agentClient, userAuthorization, authHandlerName, turnContext, authToken, CancellationToken.None); + /// public async Task AddToolServersToAgentAsync( PersistentAgentsClient agentClient, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, - string? authToken = null, - CancellationToken cancellationToken = default) + string? authToken, + CancellationToken cancellationToken) { if (agentClient == null) { @@ -133,11 +142,18 @@ public async Task AddToolServersToAgentAsync( /// /// Get MCP tool definitions and resources. /// + public Task<(IList ToolDefinitions, ToolResources? ToolResources)> GetMcpToolDefinitionsAndResourcesAsync( + string agentInstanceId, + string authToken, + ITurnContext turnContext) + => GetMcpToolDefinitionsAndResourcesAsync(agentInstanceId, authToken, turnContext, CancellationToken.None); + + /// public async Task<(IList ToolDefinitions, ToolResources? ToolResources)> GetMcpToolDefinitionsAndResourcesAsync( string agentInstanceId, string authToken, ITurnContext turnContext, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken) { // TODO: Make this method private // Tool resources should ideally be accessible via agentClient after AddToolServersToAgent. diff --git a/src/Tooling/Extensions/SemanticKernel/Services/IMcpToolRegistrationService.cs b/src/Tooling/Extensions/SemanticKernel/Services/IMcpToolRegistrationService.cs index e133e8f5..0f969e45 100644 --- a/src/Tooling/Extensions/SemanticKernel/Services/IMcpToolRegistrationService.cs +++ b/src/Tooling/Extensions/SemanticKernel/Services/IMcpToolRegistrationService.cs @@ -17,6 +17,18 @@ namespace Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services /// public interface IMcpToolRegistrationService { + /// + /// Adds the A365 MCP Tool Servers + /// + /// The kernel to which the tools will be added. + /// Agents SDK UserAuthorization System + /// Authentication Handler Name for use with the UserAuthorization System + /// + /// Auth token to access the MCP servers + /// A task that completes when the MCP tool servers have been added to the provided kernel. + /// + Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken = null); + /// /// Adds the A365 MCP Tool Servers /// @@ -26,9 +38,9 @@ public interface IMcpToolRegistrationService /// /// Auth token to access the MCP servers /// A cancellation token to cancel the operation. - /// Returns a new object of the kernel + /// A task that completes when the MCP tool servers have been added to the provided kernel. /// - Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken = null, CancellationToken cancellationToken = default); + Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken, CancellationToken cancellationToken); /// /// Sends chat history to the MCP platform for real-time threat protection. diff --git a/src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs b/src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs index 18a1fbc0..9d91e243 100644 --- a/src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs +++ b/src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs @@ -48,7 +48,11 @@ public McpToolRegistrationService( } /// - public async Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken = null, CancellationToken cancellationToken = default) + public Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken = null) + => AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext, authToken, CancellationToken.None); + + /// + public async Task AddToolServersToAgentAsync(Kernel kernel, UserAuthorization userAuthorization, string authHandlerName, ITurnContext turnContext, string? authToken, CancellationToken cancellationToken) { if (kernel == null) {