diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Tests/Services/McpClientInitializationTimeoutTests.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Tests/Services/McpClientInitializationTimeoutTests.cs
new file mode 100644
index 00000000..d712ca66
--- /dev/null
+++ b/src/Tests/Microsoft.Agents.A365.Tooling.Tests/Services/McpClientInitializationTimeoutTests.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using FluentAssertions;
+using Microsoft.Agents.A365.Tooling.Models;
+using Microsoft.Agents.A365.Tooling.Services;
+using Microsoft.Agents.Builder;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Agents.A365.Tooling.Tests.Services
+{
+ ///
+ /// Unit tests for McpClientInitializationTimeoutSeconds in ToolOptions
+ /// and its application in McpToolServerConfigurationService.
+ ///
+ public class McpClientInitializationTimeoutTests
+ {
+ private readonly Mock> _loggerMock;
+ private readonly Mock _serviceProviderMock;
+ private readonly Mock _httpClientFactoryMock;
+
+ public McpClientInitializationTimeoutTests()
+ {
+ _loggerMock = new Mock>();
+ _serviceProviderMock = new Mock();
+ _httpClientFactoryMock = new Mock();
+
+ _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny()))
+ .Returns(new HttpClient());
+ }
+
+ [Fact]
+ public void ToolOptions_McpClientInitializationTimeoutSeconds_DefaultsToNull()
+ {
+ // Arrange & Act
+ var options = new ToolOptions();
+
+ // Assert
+ options.McpClientInitializationTimeoutSeconds.Should().BeNull();
+ }
+
+ [Fact]
+ public void ToolOptions_McpClientInitializationTimeoutSeconds_CanBeSetToValidValue()
+ {
+ // Arrange & Act
+ var options = new ToolOptions
+ {
+ McpClientInitializationTimeoutSeconds = 180
+ };
+
+ // Assert
+ options.McpClientInitializationTimeoutSeconds.Should().Be(180);
+ }
+
+ [Fact]
+ public void ToolOptions_McpClientInitializationTimeoutSeconds_CanBeSetToNull()
+ {
+ // Arrange
+ var options = new ToolOptions
+ {
+ McpClientInitializationTimeoutSeconds = 120
+ };
+
+ // Act
+ options.McpClientInitializationTimeoutSeconds = null;
+
+ // Assert
+ options.McpClientInitializationTimeoutSeconds.Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ [InlineData(-100)]
+ [InlineData(601)]
+ [InlineData(int.MaxValue)]
+ public async Task GetMcpClientToolsAsync_InvalidTimeoutValue_ThrowsArgumentOutOfRangeException(int invalidTimeout)
+ {
+ // Arrange
+ var configMock = new Mock();
+ configMock.Setup(c => c["ASPNETCORE_ENVIRONMENT"]).Returns("Development");
+
+ var service = new McpToolServerConfigurationService(
+ _loggerMock.Object,
+ configMock.Object,
+ _serviceProviderMock.Object,
+ _httpClientFactoryMock.Object);
+
+ var serverConfig = new MCPServerConfig
+ {
+ mcpServerName = "test_server",
+ url = "https://localhost:52856/agents/servers/test_server",
+ id = "test-id",
+ scope = "test-scope",
+ audience = "test-audience",
+ publisher = "test-publisher",
+ };
+
+ var toolOptions = new ToolOptions
+ {
+ McpClientInitializationTimeoutSeconds = invalidTimeout
+ };
+
+ var turnContextMock = new Mock();
+
+ // Act
+ Func act = async () => await service.GetMcpClientToolsAsync(
+ turnContextMock.Object, serverConfig, "test-token", toolOptions);
+
+ // Assert - invalid timeout should surface an ArgumentOutOfRangeException either directly
+ // or wrapped by a higher-level InvalidOperationException depending on the call path.
+ var exception = await Record.ExceptionAsync(act);
+ exception.Should().NotBeNull();
+ (exception is ArgumentOutOfRangeException
+ || exception is InvalidOperationException { InnerException: ArgumentOutOfRangeException })
+ .Should().BeTrue("an invalid timeout should result in an ArgumentOutOfRangeException, either directly or wrapped");
+ }
+
+ [Fact]
+ public void GetValidatedInitializationTimeout_NullInput_ReturnsNull()
+ {
+ // Act
+ var result = McpToolServerConfigurationService.GetValidatedInitializationTimeout(null);
+
+ // Assert
+ result.Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(60)]
+ [InlineData(120)]
+ [InlineData(300)]
+ [InlineData(600)]
+ public void GetValidatedInitializationTimeout_ValidInput_ReturnsMatchingTimeSpan(int seconds)
+ {
+ // Act
+ var result = McpToolServerConfigurationService.GetValidatedInitializationTimeout(seconds);
+
+ // Assert
+ result.Should().Be(TimeSpan.FromSeconds(seconds));
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ [InlineData(-100)]
+ [InlineData(601)]
+ [InlineData(int.MaxValue)]
+ public void GetValidatedInitializationTimeout_InvalidInput_ThrowsArgumentOutOfRangeException(int invalidSeconds)
+ {
+ // Act
+ Action act = () => McpToolServerConfigurationService.GetValidatedInitializationTimeout(invalidSeconds);
+
+ // Assert
+ act.Should().Throw()
+ .Which.ParamName.Should().Be(nameof(ToolOptions.McpClientInitializationTimeoutSeconds));
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(60)]
+ [InlineData(120)]
+ [InlineData(300)]
+ [InlineData(600)]
+ public void ToolOptions_McpClientInitializationTimeoutSeconds_AcceptsValidValues(int validTimeout)
+ {
+ // Arrange & Act
+ var options = new ToolOptions
+ {
+ McpClientInitializationTimeoutSeconds = validTimeout
+ };
+
+ // Assert
+ options.McpClientInitializationTimeoutSeconds.Should().Be(validTimeout);
+ }
+ }
+}
diff --git a/src/Tooling/Core/Models/ToolOptions.cs b/src/Tooling/Core/Models/ToolOptions.cs
index 3a63f213..ade9bf3b 100644
--- a/src/Tooling/Core/Models/ToolOptions.cs
+++ b/src/Tooling/Core/Models/ToolOptions.cs
@@ -14,5 +14,15 @@ public class ToolOptions
/// Gets or sets the user agent configuration for this orchestrator.
///
public IUserAgentConfiguration? UserAgentConfiguration { get; set; }
+
+ ///
+ /// Gets or sets the timeout in seconds for MCP client initialization.
+ /// This includes the time for the MCP protocol handshake (initialize/initialized exchange)
+ /// and the underlying HTTP connection. Increase this value if the MCP server performs
+ /// slow operations during initialization (e.g., token exchanges in test environments).
+ /// When null, the MCP SDK default timeout is used.
+ /// Valid range: 1 to 600 seconds. Values outside this range will throw .
+ ///
+ public int? McpClientInitializationTimeoutSeconds { get; set; }
}
}
diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs
index f5179dab..fd0ea672 100644
--- a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs
+++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs
@@ -521,13 +521,34 @@ private async Task CreateMcpClientWithAuthHandlers(ITurnContext turn
TransportMode = HttpTransportMode.AutoDetect,
};
+ // Validate and compute the initialization timeout once so HttpClient.Timeout
+ // and McpClientOptions.InitializationTimeout always agree.
+ var initializationTimeout = GetValidatedInitializationTimeout(toolOptions.McpClientInitializationTimeoutSeconds);
+
// Create HTTP client with the authentication handler chain
var httpClient = new HttpClient(loggingHandler);
+ // Apply custom timeout only when explicitly configured
+ if (initializationTimeout.HasValue)
+ {
+ httpClient.Timeout = initializationTimeout.Value;
+ }
+
var clientTransport = new SseClientTransport(options, httpClient);
try
{
+ // Only pass McpClientOptions when a custom timeout is set to preserve default SDK behavior
+ if (initializationTimeout.HasValue)
+ {
+ var clientOptions = new McpClientOptions
+ {
+ InitializationTimeout = initializationTimeout.Value,
+ };
+
+ return await McpClientFactory.CreateAsync(clientTransport, clientOptions, loggerFactory: this._loggerFactory);
+ }
+
return await McpClientFactory.CreateAsync(clientTransport, loggerFactory: this._loggerFactory);
}
catch (Exception ex)
@@ -536,6 +557,35 @@ private async Task CreateMcpClientWithAuthHandlers(ITurnContext turn
}
}
+ ///
+ /// Validates and converts it
+ /// to a . Returns null when no custom timeout is configured so
+ /// callers can preserve the MCP SDK default behavior.
+ ///
+ /// The configured timeout in seconds, or null to use the SDK default.
+ /// The validated timeout as a , or null when not configured.
+ ///
+ /// Thrown when is set but falls outside the supported range of 1 to 600 seconds.
+ ///
+ internal static TimeSpan? GetValidatedInitializationTimeout(int? timeoutSeconds)
+ {
+ if (!timeoutSeconds.HasValue)
+ {
+ return null;
+ }
+
+ var value = timeoutSeconds.Value;
+ if (value < 1 || value > 600)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(ToolOptions.McpClientInitializationTimeoutSeconds),
+ value,
+ "McpClientInitializationTimeoutSeconds must be between 1 and 600 seconds.");
+ }
+
+ return TimeSpan.FromSeconds(value);
+ }
+
///
/// Attaches a per-audience Bearer token to each server's
/// dictionary.