Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
58a7c9d
Migrate Foundry to external MCP server
g2vinay Feb 19, 2026
4688c8a
Merge remote-tracking branch 'upstream/main' into add-foundry-mcp-server
g2vinay Feb 19, 2026
b1d8abc
auth update add browser fallback
g2vinay Feb 24, 2026
9b7c48b
Add forceBrowserFallback and normalizeScopes for registry server OAuth
g2vinay Feb 25, 2026
07e8650
Merge remote-tracking branch 'upstream/main' into add-foundry-mcp-server
g2vinay Feb 25, 2026
5c0e396
Add changelog entry for Foundry external MCP server migration
g2vinay Feb 25, 2026
687c98c
Add foundry extensions
g2vinay Mar 3, 2026
b49f342
Merge remote-tracking branch 'upstream/main' into add-foundry-mcp-server
g2vinay Mar 3, 2026
25987d9
Fix failing tests after foundry registry and CustomChainedCredential …
g2vinay Mar 3, 2026
2914674
Address feedback
g2vinay Mar 3, 2026
3ff53f9
Fix whitespace formatting and improve VisualStudioToolNameTests robus…
g2vinay Mar 3, 2026
3d9c1ec
Add changelog entry for FoundryExtensions namespace move and new exte…
g2vinay Mar 3, 2026
4fe31f5
Rename changelog entry to timestamp-based filename
g2vinay Mar 3, 2026
27c0318
Consolidate changelog entries into single file
g2vinay Mar 3, 2026
6d3019b
add changelog
g2vinay Mar 3, 2026
16c94ce
Add CODEOWNERS entry for FoundryExtensions toolset
g2vinay Mar 3, 2026
0cb02ae
Fix: restrict FoundryExtensions tests to --namespace foundryextension…
g2vinay Mar 3, 2026
6202ec1
Fix: skip external registry servers when running under TEST_PROXY_URL…
g2vinay Mar 3, 2026
6af8f9a
Add FoundryExtensionsSetup to CommandFactoryHelpers
g2vinay Mar 3, 2026
cbe68ba
update docs + fix unit tests
g2vinay Mar 3, 2026
89f0a31
update docs + fix unit tests
g2vinay Mar 3, 2026
ba2c4a0
Register FoundryExtensionsSetup in Program.cs and add solution structure
g2vinay Mar 3, 2026
8a67e0c
Fix: Remove stale foundry_agents_create from consolidated-tools.json
g2vinay Mar 3, 2026
731ca0e
fix consolidated mode + code cleanup
g2vinay Mar 4, 2026
74b9616
Sync with upstream, update solution file, and re-record FoundryExtens…
g2vinay Mar 4, 2026
f1b2cc9
Merge remote-tracking branch 'upstream/main' into add-foundry-mcp-server
g2vinay Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,10 @@
# ServiceLabel: %tools-FileShares
# ServiceOwners: @ankushbindlish2 @kszobi

# PRLabel: %tools-Foundry
/tools/Azure.Mcp.Tools.Foundry/ @jayzzh @xiangyan99 @microsoft/azure-mcp
# PRLabel: %tools-FoundryExtensions
/tools/Azure.Mcp.Tools.FoundryExtensions/ @jayzzh @xiangyan99 @microsoft/azure-mcp

# ServiceLabel: %tools-Foundry
# ServiceLabel: %tools-FoundryExtensions
# ServiceOwners: @jayzzh @xiangyan99

# PRLabel: %tools-FunctionApp
Expand Down
14 changes: 8 additions & 6 deletions Microsoft.Mcp.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,16 @@
<Project Path="tools/Azure.Mcp.Tools.FileShares/tests/Azure.Mcp.Tools.FileShares.LiveTests/Azure.Mcp.Tools.FileShares.LiveTests.csproj" />
<Project Path="tools/Azure.Mcp.Tools.FileShares/tests/Azure.Mcp.Tools.FileShares.UnitTests/Azure.Mcp.Tools.FileShares.UnitTests.csproj" />
</Folder>
<Folder Name="/tools/Azure.Mcp.Tools.Foundry/" />
<Folder Name="/tools/Azure.Mcp.Tools.Foundry/src/">
<Project Path="tools/Azure.Mcp.Tools.Foundry/src/Azure.Mcp.Tools.Foundry.csproj" />

<Folder Name="/tools/Azure.Mcp.Tools.FoundryExtensions/" />
<Folder Name="/tools/Azure.Mcp.Tools.FoundryExtensions/src/">
<Project Path="tools/Azure.Mcp.Tools.FoundryExtensions/src/Azure.Mcp.Tools.FoundryExtensions.csproj" />
</Folder>
<Folder Name="/tools/Azure.Mcp.Tools.Foundry/tests/">
<Project Path="tools/Azure.Mcp.Tools.Foundry/tests/Azure.Mcp.Tools.Foundry.LiveTests/Azure.Mcp.Tools.Foundry.LiveTests.csproj" />
<Project Path="tools/Azure.Mcp.Tools.Foundry/tests/Azure.Mcp.Tools.Foundry.UnitTests/Azure.Mcp.Tools.Foundry.UnitTests.csproj" />
<Folder Name="/tools/Azure.Mcp.Tools.FoundryExtensions/tests/">
<Project Path="tools/Azure.Mcp.Tools.FoundryExtensions/tests/Azure.Mcp.Tools.FoundryExtensions.LiveTests/Azure.Mcp.Tools.FoundryExtensions.LiveTests.csproj" />
<Project Path="tools/Azure.Mcp.Tools.FoundryExtensions/tests/Azure.Mcp.Tools.FoundryExtensions.UnitTests/Azure.Mcp.Tools.FoundryExtensions.UnitTests.csproj" />
</Folder>

<Folder Name="/tools/Azure.Mcp.Tools.FunctionApp/" />
<Folder Name="/tools/Azure.Mcp.Tools.FunctionApp/src/">
<Project Path="tools/Azure.Mcp.Tools.FunctionApp/src/Azure.Mcp.Tools.FunctionApp.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
using Azure.Mcp.Tools.Deploy;
using Azure.Mcp.Tools.EventGrid;
using Azure.Mcp.Tools.Extension;
using Azure.Mcp.Tools.Foundry;
using Azure.Mcp.Tools.FoundryExtensions;
using Azure.Mcp.Tools.FunctionApp;
using Azure.Mcp.Tools.Grafana;
using Azure.Mcp.Tools.KeyVault;
Expand Down Expand Up @@ -76,7 +76,7 @@ public static ICommandFactory CreateCommandFactory(IServiceProvider? serviceProv
new DeploySetup(),
new EventGridSetup(),
new ExtensionSetup(),
new FoundrySetup(),
new FoundryExtensionsSetup(),
new FunctionAppSetup(),
new GrafanaSetup(),
new KeyVaultSetup(),
Expand Down Expand Up @@ -140,7 +140,7 @@ public static IServiceCollection SetupCommonServices()
new DeploySetup(),
new EventGridSetup(),
new ExtensionSetup(),
new FoundrySetup(),
new FoundryExtensionsSetup(),
new FunctionAppSetup(),
new GrafanaSetup(),
new KeyVaultSetup(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,68 @@ public async Task DiscoverServersAsync_NamespaceFilteringIsCaseInsensitive()
var serverIds = providers.Select(p => p.CreateMetadata().Id).ToList();
Assert.Contains("documentation", serverIds);
}

[Fact]
public async Task DiscoverServersAsync_FoundryServerIsDiscovered()
{
// Arrange
var strategy = RegistryDiscoveryStrategyHelper.CreateStrategy();

// Act
var result = await strategy.DiscoverServersAsync(TestContext.Current.CancellationToken);
var foundryProvider = result.FirstOrDefault(p => p.CreateMetadata().Name == "foundry");

// Assert
Assert.NotNull(foundryProvider);

var metadata = foundryProvider.CreateMetadata();
Assert.Equal("foundry", metadata.Id);
Assert.Equal("foundry", metadata.Name);
Assert.NotEmpty(metadata.Description);

// Verify description contains key terms
var description = metadata.Description.ToLowerInvariant();
Assert.Contains("foundry", description);
Assert.Contains("mcp", description);
}

[Fact]
public async Task DiscoverServersAsync_FoundryServerHasExpectedProperties()
{
// Arrange
var strategy = RegistryDiscoveryStrategyHelper.CreateStrategy();

// Act
var result = await strategy.DiscoverServersAsync(TestContext.Current.CancellationToken);
var foundryProvider = result.FirstOrDefault(p => p.CreateMetadata().Name == "foundry");

// Assert
Assert.NotNull(foundryProvider);

var metadata = foundryProvider.CreateMetadata();
Assert.Equal("foundry", metadata.Id);
Assert.Equal("foundry", metadata.Name);

// Description should mention models, agents, and evaluation workflows
var description = metadata.Description.ToLowerInvariant();
Assert.Contains("models", description);
Assert.Contains("agents", description);
}

[Fact]
public async Task DiscoverServersAsync_AllExpectedServersArePresent()
{
// Arrange
var strategy = RegistryDiscoveryStrategyHelper.CreateStrategy();

// Act
var result = await strategy.DiscoverServersAsync(TestContext.Current.CancellationToken);
var serverIds = result.Select(p => p.CreateMetadata().Id).ToList();

// Assert
// Verify all expected registry servers are discovered
Assert.Contains("documentation", serverIds);
Assert.Contains("azd", serverIds);
Assert.Contains("foundry", serverIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,15 @@ private static TokenCredential CreateCustomChainedCredential()
.FirstOrDefault(c =>
{
var parameters = c.GetParameters();
return parameters.Length == 2 &&
return parameters.Length == 3 &&
parameters[0].ParameterType == typeof(string) &&
parameters[1].ParameterType == typeof(ILogger<>).MakeGenericType(customChainedCredentialType);
parameters[1].ParameterType == typeof(ILogger<>).MakeGenericType(customChainedCredentialType) &&
parameters[2].ParameterType == typeof(bool);
});

Assert.NotNull(constructor);

var credential = constructor.Invoke([null, null]) as TokenCredential;
var credential = constructor.Invoke([null, null, false]) as TokenCredential;
Assert.NotNull(credential);

return credential;
Expand Down
12 changes: 10 additions & 2 deletions core/Microsoft.Mcp.Core/src/AccessTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ namespace Azure.Mcp.Core;
/// </summary>
public sealed class AccessTokenHandler : DelegatingHandler
{
private readonly IAzureTokenCredentialProvider _tokenCredentialProvider;
private readonly IAzureTokenCredentialProvider? _tokenCredentialProvider;
private readonly TokenCredential? _credential;
private readonly string[] _oauthScopes;

public AccessTokenHandler(IAzureTokenCredentialProvider tokenCredentialProvider, string[] oauthScopes)
Expand All @@ -21,6 +22,12 @@ public AccessTokenHandler(IAzureTokenCredentialProvider tokenCredentialProvider,
_oauthScopes = oauthScopes;
}

public AccessTokenHandler(TokenCredential credential, string[] oauthScopes)
{
_credential = credential;
_oauthScopes = oauthScopes;
}

/// <summary>
/// Sends an HTTP request with a Bearer access token fetched using the embedded <see cref="IAzureTokenCredentialProvider"/>.
/// This method will overwrite the Authorization header if it already exist on the request.
Expand All @@ -29,7 +36,8 @@ public AccessTokenHandler(IAzureTokenCredentialProvider tokenCredentialProvider,
/// <param name="cancellationToken"></param>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var credential = await _tokenCredentialProvider.GetTokenCredentialAsync(tenantId: null, cancellationToken);
TokenCredential credential = _credential
?? await _tokenCredentialProvider!.GetTokenCredentialAsync(tenantId: null, cancellationToken);
var tokenContext = new TokenRequestContext(_oauthScopes);
var token = await credential.GetTokenAsync(tokenContext, cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@ private async Task InitializeAsync(CancellationToken cancellationToken)
return;
}

// When running under a test proxy (TEST_PROXY_URL is set by the test infrastructure),
// every outgoing HTTP request is redirected through the proxy by RecordingRedirectHandler.
// External registry server connections (e.g. mcp.ai.azure.com) would therefore hit the
// test proxy during an active recording/playback session, either producing unrecorded
// traffic in playback mode or polluting the recording sequence in record mode. Skip
// registry initialization entirely so only the local in-process tools are loaded.
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TEST_PROXY_URL")))
{
_logger.LogDebug("Skipping registry server initialization: TEST_PROXY_URL is set (running under test proxy).");
_isInitialized = true;
return;
}

await _initializationSemaphore.WaitAsync(cancellationToken);
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,20 @@ public static IServiceCollection AddRegistryRoot(this IServiceCollection service
}

services.AddHttpClient(RegistryServerHelper.GetRegistryServerHttpClientName(serverName))
.AddHttpMessageHandler((services) =>
.AddHttpMessageHandler((sp) =>
{
var tokenCredentialProvider = services.GetRequiredService<IAzureTokenCredentialProvider>();
return new AccessTokenHandler(tokenCredentialProvider, oauthScopes);
var provider = sp.GetRequiredService<IAzureTokenCredentialProvider>();
// Only force browser fallback for SingleIdentityTokenCredentialProvider
// (stdio mode and UseHostingEnvironmentIdentity HTTP mode). In those scenarios
// the user's own identity drives auth, so an interactive browser prompt is a
// reasonable last resort when silent credentials (AzCLI, WAM, etc.) fail.
// For UseOnBehalfOf (HttpOnBehalfOfTokenCredentialProvider) the OBO flow owns
// the token exchange — delegate back to the provider as usual.
if (provider is SingleIdentityTokenCredentialProvider)
{
return new AccessTokenHandler(new CustomChainedCredential(forceBrowserFallback: true), oauthScopes);
}
return new AccessTokenHandler(provider, oauthScopes);
});
}

Expand Down
Loading
Loading