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 5 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
@@ -0,0 +1,30 @@
using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting;

/// <summary>
/// Options for configuring the MCP Inspector resource.
/// </summary>
public class McpInspectorOptions
{
/// <summary>
/// Gets or sets the port for the client application. Defaults to "6274".
/// </summary>
public int ClientPort { get; set; } = 6274;

/// <summary>
/// Gets or sets the port for the server proxy application. Defaults to "6277".
/// </summary>
public int ServerPort { get; set; } = 6277;

/// <summary>
/// Gets or sets the version of the Inspector app to use. Defaults to <see cref="McpInspectorResource.InspectorVersion"/>.
/// </summary>
public string InspectorVersion { get; set; } = McpInspectorResource.InspectorVersion;

/// <summary>
/// Gets or sets the parameter used to provide the proxy authentication token for the MCP Inspector resource.
/// If <see langword="null"/> a random token will be generated.
/// </summary>
public IResourceBuilder<ParameterResource>? ProxyToken { get; set; }
}
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,39 @@ 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>
[Obsolete("Use the overload with McpInspectorOptions instead. This overload will be removed in the next version.")]
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion)
{
ArgumentNullException.ThrowIfNull(builder);

return AddMcpInspector(builder, name, options =>
{
options.ClientPort = clientPort;
options.ServerPort = serverPort;
options.InspectorVersion = inspectorVersion;
});
}

/// <summary>
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using an options object.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
/// <param name="name">The name of the MCP Inspector container resource.</param>
/// <param name="options">The <see cref="McpInspectorOptions"/> to configure the MCP Inspector resource.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, McpInspectorOptions options)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(options);

var proxyTokenParameter = options.ProxyToken?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-proxyToken");

var resource = builder.AddResource(new McpInspectorResource(name))
.WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"])
.WithArgs(["-y", $"@modelcontextprotocol/inspector@{options.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")
.WithHttpEndpoint(isProxied: false, port: options.ClientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName)
.WithHttpEndpoint(isProxied: false, port: options.ServerPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName)
.WithHttpHealthCheck("/", endpointName: McpInspectorResource.ClientEndpointName)
.WithHttpHealthCheck("/config", endpointName: McpInspectorResource.ServerProxyEndpointName)
.WithUrlForEndpoint(McpInspectorResource.ClientEndpointName, annotation =>
{
annotation.DisplayText = "Client";
Expand All @@ -34,7 +58,45 @@ 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) =>
{
Expand Down Expand Up @@ -80,6 +142,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 +153,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 All @@ -103,6 +165,39 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
});
}

/// <summary>
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using a configuration delegate.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
/// <param name="name">The name of the MCP Inspector container resource.</param>
/// <param name="configureOptions">A delegate to configure the <see cref="McpInspectorOptions"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, Action<McpInspectorOptions> configureOptions)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configureOptions);

var options = new McpInspectorOptions();
configureOptions(options);

return builder.AddMcpInspector(name, options);
}

/// <summary>
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using a configuration delegate.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
/// <param name="name">The name of the MCP Inspector container resource.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name)
{
ArgumentNullException.ThrowIfNull(builder);

var options = new McpInspectorOptions();

return builder.AddMcpInspector(name, options);
}

/// <summary>
/// Configures the MCP Inspector resource to use a specified MCP server resource that uses SSE as the transport type.
/// </summary>
Expand Down
44 changes: 42 additions & 2 deletions src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,52 @@ var inspector = builder.AddMcpInspector("inspector")
.WithMcpServer(mcpServer);
```

You can specify the transport type (`StreamableHttp` or `Sse`) and set which server is the default for the inspector.
You can specify the transport type (`StreamableHttp`) and set which server is the default for the inspector.

#### Using options for complex configurations

For more complex configurations with multiple parameters, you can use the options-based approach:

```csharp
var customToken = builder.AddParameter("custom-proxy-token", secret: true);

var options = new McpInspectorOptions
{
ClientPort = 6275,
ServerPort = 6278,
InspectorVersion = "0.16.2",
ProxyToken = customToken
};

var inspector = builder.AddMcpInspector("inspector", options)
.WithMcpServer(mcpServer);
```

Alternatively, you can use a configuration delegate for a more fluent approach:

```csharp
var inspector = builder.AddMcpInspector("inspector", options =>
{
options.ClientPort = 6275;
options.ServerPort = 6278;
options.InspectorVersion = "0.16.2";
})
.WithMcpServer(mcpServer);
```

#### Configuration options

The `McpInspectorOptions` class provides the following configuration properties:

- `ClientPort`: Port for the client application (default: 6274
- `ServerPort`: Port for the server proxy application (default: 6277)
- `InspectorVersion`: Version of the Inspector app to use (default: latest supported version)
- `ProxyToken`: Custom authentication token parameter (default: auto-generated)

## Additional Information

See the [official documentation](https://learn.microsoft.com/dotnet/aspire/community-toolkit/mcpinspector) for more details.

## Feedback & contributing

https://github.com/CommunityToolkit/Aspire
[https://github.com/CommunityToolkit/Aspire](https://github.com/CommunityToolkit/Aspire)
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
Loading
Loading