Skip to content

Support auth token on MCP Inspector #780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public class McpInspectorResource(string name) : ExecutableResource(name, "npx",
/// </summary>
public McpServerMetadata? DefaultMcpServer => _defaultMcpServer;

/// <summary>
/// Gets or sets the parameter that contains the MCP proxy authentication token.
/// </summary>
public ParameterResource ProxyTokenParameter { get; set; } = default!;

internal void AddMcpServer(IResourceWithEndpoints mcpServer, bool isDefault, McpTransportType transportType)
{
if (_mcpServers.Any(s => s.Name == mcpServer.Name))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting;

Expand All @@ -15,16 +16,17 @@ public static class McpInspectorResourceBuilderExtensions
/// <param name="clientPort">The port for the client application. Defaults to 6274.</param>
/// <param name="serverPort">The port for the server proxy application. Defaults to 6277.</param>
/// <param name="inspectorVersion">The version of the Inspector app to use</param>
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion)
/// <param name="proxyToken">The parameter used to provide the proxy authentication token for the MCP Inspector resource. If <see langword="null"/> a random token will be generated.</param>
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion, IResourceBuilder<ParameterResource>? proxyToken = null)
{
var proxyTokenParameter = proxyToken?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-proxyToken");

var resource = builder.AddResource(new McpInspectorResource(name))
.WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"])
.ExcludeFromManifest()
.WithHttpEndpoint(isProxied: false, port: clientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName)
.WithHttpEndpoint(isProxied: false, port: serverPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName)
.WithEnvironment("DANGEROUSLY_OMIT_AUTH", "true")
.WithHttpHealthCheck("/", endpointName: McpInspectorResource.ClientEndpointName)
.WithHttpHealthCheck("/config", endpointName: McpInspectorResource.ServerProxyEndpointName)
.WithUrlForEndpoint(McpInspectorResource.ClientEndpointName, annotation =>
{
annotation.DisplayText = "Client";
Expand All @@ -34,8 +36,46 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
{
annotation.DisplayText = "Server Proxy";
annotation.DisplayOrder = 1;
annotation.DisplayLocation = UrlDisplayLocation.DetailsOnly;
})
.WithUrls(async context =>
{
var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None);

foreach (var url in context.Urls)
{
if (url.Endpoint is not null)
{
var uriBuilder = new UriBuilder(url.Url);
uriBuilder.Query = $"MCP_PROXY_AUTH_TOKEN={Uri.EscapeDataString(token!)}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmmm, interesting. This might not be the best practice right?

cc @DamianEdwards

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It definitely feels dirty. No link and defer to getting it (the link) rom console log?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, this is the URI that is emitted in the console...so it's not like this surfaces a secret of sorts that isn't surfaced already.

url.Url = uriBuilder.ToString();
}
}
});

resource.Resource.ProxyTokenParameter = proxyTokenParameter;

// Add authenticated health check for server proxy /config endpoint
var healthCheckKey = $"{name}_proxy_config_check";
builder.Services.AddHealthChecks().AddUrlGroup(options =>
{
var serverProxyEndpoint = resource.GetEndpoint(McpInspectorResource.ServerProxyEndpointName);
var uri = serverProxyEndpoint.Url;
if (uri is null)
{
throw new DistributedApplicationException("The MCP Inspector 'server-proxy' endpoint URL is not set. Ensure that the resource has been allocated before the health check is executed.");
}

var healthCheckUri = new Uri(new Uri(uri), "/config");
options.AddUri(healthCheckUri, async setup =>
{
var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None);
setup.AddCustomHeader("X-MCP-Proxy-Auth", $"Bearer {token}");
});
}, healthCheckKey);

resource.WithHealthCheck(healthCheckKey);

builder.Eventing.Subscribe<BeforeResourceStartedEvent>(resource.Resource, async (@event, ct) =>
{
if (@event.Resource is not McpInspectorResource inspectorResource)
Expand Down Expand Up @@ -80,6 +120,7 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
ctx.EnvironmentVariables["MCP_PROXY_FULL_ADDRESS"] = serverProxyEndpoint.Url;
ctx.EnvironmentVariables["CLIENT_PORT"] = clientEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'client' endpoint must have a target port defined.");
ctx.EnvironmentVariables["SERVER_PORT"] = serverProxyEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'server-proxy' endpoint must have a target port defined.");
ctx.EnvironmentVariables["MCP_PROXY_AUTH_TOKEN"] = proxyTokenParameter;
})
.WithArgs(ctx =>
{
Expand All @@ -90,7 +131,6 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
throw new InvalidOperationException("No default MCP server has been configured for the MCP Inspector resource, yet servers have been provided.");
}


if (defaultMcpServer is null && inspectorResource.McpServers.Count == 0)
{
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Testing;

namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests;

public class AppHostTests(AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_McpInspector_AppHost> fixture) : IClassFixture<AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_McpInspector_AppHost>>
{
[Theory]
[InlineData(McpInspectorResource.ClientEndpointName, "/")]
[InlineData(McpInspectorResource.ServerProxyEndpointName, "/config")]
public async Task ResourceStartsAndRespondsOk(string endpointName, string route)
[Fact]
public async Task ClientEndpointStartsAndRespondsOk()
{
var resourceName = "mcp-inspector";
await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5));
var httpClient = fixture.CreateHttpClient(resourceName, endpointName: endpointName);
var httpClient = fixture.CreateHttpClient(resourceName, endpointName: McpInspectorResource.ClientEndpointName);

var response = await httpClient.GetAsync(route);
var response = await httpClient.GetAsync("/");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

[Fact]
public async Task ServerProxyConfigEndpointWithAuthRespondsOk()
{
var resourceName = "mcp-inspector";
await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5));

// Get the MCP Inspector resource to access the proxy token parameter
var appModel = fixture.App.Services.GetRequiredService<DistributedApplicationModel>();
var mcpInspectorResource = appModel.Resources.OfType<McpInspectorResource>().Single(r => r.Name == resourceName);

// Get the token value
var token = await mcpInspectorResource.ProxyTokenParameter.GetValueAsync(CancellationToken.None);

var httpClient = fixture.CreateHttpClient(resourceName, endpointName: McpInspectorResource.ServerProxyEndpointName);

// Add the Bearer token header for authentication
httpClient.DefaultRequestHeaders.Add("X-MCP-Proxy-Auth", $"Bearer {token}");

var response = await httpClient.GetAsync("/config");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;

namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests;

Expand Down Expand Up @@ -116,4 +117,69 @@ public void WithMultipleMcpServersAddsAllServersToResource()
Assert.NotNull(inspectorResource.DefaultMcpServer);
Assert.Equal("mcpServer1", inspectorResource.DefaultMcpServer.Name);
}

[Fact]
public void AddMcpInspectorGeneratesProxyTokenParameter()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

// Act
var inspector = appBuilder.AddMcpInspector("inspector");

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());
Assert.NotNull(inspectorResource.ProxyTokenParameter);
Assert.Equal("inspector-proxyToken", inspectorResource.ProxyTokenParameter.Name);
}

[Fact]
public void AddMcpInspectorWithCustomProxyTokenUsesProvidedToken()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();
var customToken = appBuilder.AddParameter("custom-token", secret: true);

// Act
var inspector = appBuilder.AddMcpInspector("inspector", proxyToken: customToken);

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());
Assert.NotNull(inspectorResource.ProxyTokenParameter);
Assert.Equal("custom-token", inspectorResource.ProxyTokenParameter.Name);
Assert.Same(customToken.Resource, inspectorResource.ProxyTokenParameter);
}

[Fact]
public void AddMcpInspectorSetsCorrectEnvironmentVariables()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

// Act
var inspector = appBuilder.AddMcpInspector("inspector", clientPort: 1234, serverPort: 5678);

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());

// Verify endpoints are configured correctly
var clientEndpoint = inspectorResource.Annotations.OfType<EndpointAnnotation>()
.Single(e => e.Name == McpInspectorResource.ClientEndpointName);
var serverEndpoint = inspectorResource.Annotations.OfType<EndpointAnnotation>()
.Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName);

Assert.Equal(1234, clientEndpoint.Port);
Assert.Equal(5678, serverEndpoint.Port);
}
}
Loading