diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj b/src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj
index de12cbf0..3aa5b4a8 100644
--- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj
+++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj
@@ -6,6 +6,7 @@
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorOptions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorOptions.cs
new file mode 100644
index 00000000..7932c679
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorOptions.cs
@@ -0,0 +1,30 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting;
+
+///
+/// Options for configuring the MCP Inspector resource.
+///
+public class McpInspectorOptions
+{
+ ///
+ /// Gets or sets the port for the client application. Defaults to "6274".
+ ///
+ public int ClientPort { get; set; } = 6274;
+
+ ///
+ /// Gets or sets the port for the server proxy application. Defaults to "6277".
+ ///
+ public int ServerPort { get; set; } = 6277;
+
+ ///
+ /// Gets or sets the version of the Inspector app to use. Defaults to .
+ ///
+ public string InspectorVersion { get; set; } = McpInspectorResource.InspectorVersion;
+
+ ///
+ /// Gets or sets the parameter used to provide the proxy authentication token for the MCP Inspector resource.
+ /// If a random token will be generated.
+ ///
+ public IResourceBuilder? ProxyToken { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs
index c014a7bf..16d99a33 100644
--- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs
@@ -53,6 +53,11 @@ public class McpInspectorResource(string name) : ExecutableResource(name, "npx",
///
public McpServerMetadata? DefaultMcpServer => _defaultMcpServer;
+ ///
+ /// Gets or sets the parameter that contains the MCP proxy authentication token.
+ ///
+ public ParameterResource ProxyTokenParameter { get; set; } = default!;
+
internal void AddMcpServer(IResourceWithEndpoints mcpServer, bool isDefault, McpTransportType transportType)
{
if (_mcpServers.Any(s => s.Name == mcpServer.Name))
diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs
index c5d062e8..1ab5405c 100644
--- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs
@@ -1,4 +1,5 @@
using Aspire.Hosting.ApplicationModel;
+using Microsoft.Extensions.DependencyInjection;
namespace Aspire.Hosting;
@@ -15,16 +16,39 @@ public static class McpInspectorResourceBuilderExtensions
/// The port for the client application. Defaults to 6274.
/// The port for the server proxy application. Defaults to 6277.
/// The version of the Inspector app to use
+ [Obsolete("Use the overload with McpInspectorOptions instead. This overload will be removed in the next version.")]
public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion)
{
- return builder.AddResource(new McpInspectorResource(name))
- .WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"])
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return AddMcpInspector(builder, name, options =>
+ {
+ options.ClientPort = clientPort;
+ options.ServerPort = serverPort;
+ options.InspectorVersion = inspectorVersion;
+ });
+ }
+
+ ///
+ /// Adds a MCP Inspector container resource to the using an options object.
+ ///
+ /// The to which the MCP Inspector resource will be added.
+ /// The name of the MCP Inspector container resource.
+ /// The to configure the MCP Inspector resource.
+ /// A reference to the for further configuration.
+ public static IResourceBuilder 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@{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)
.WithEnvironment("MCP_AUTO_OPEN_ENABLED", "false")
.WithUrlForEndpoint(McpInspectorResource.ClientEndpointName, annotation =>
{
@@ -35,6 +59,7 @@ public static IResourceBuilder AddMcpInspector(this IDistr
{
annotation.DisplayText = "Server Proxy";
annotation.DisplayOrder = 1;
+ annotation.DisplayLocation = UrlDisplayLocation.DetailsOnly;
})
.OnBeforeResourceStarted(async (inspectorResource, @event, ct) =>
{
@@ -78,8 +103,76 @@ public static IResourceBuilder 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;
})
- .WithDefaultArgs();
+ .WithDefaultArgs()
+ .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)
+ {
+ Query = $"MCP_PROXY_AUTH_TOKEN={Uri.EscapeDataString(token!)}"
+ };
+ 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 ?? 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);
+
+ return resource.WithHealthCheck(healthCheckKey);
+ }
+
+ ///
+ /// Adds a MCP Inspector container resource to the using a configuration delegate.
+ ///
+ /// The to which the MCP Inspector resource will be added.
+ /// The name of the MCP Inspector container resource.
+ /// A delegate to configure the .
+ /// A reference to the for further configuration.
+ public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new McpInspectorOptions();
+ configureOptions(options);
+
+ return builder.AddMcpInspector(name, options);
+ }
+
+ ///
+ /// Adds a MCP Inspector container resource to the using a configuration delegate.
+ ///
+ /// The to which the MCP Inspector resource will be added.
+ /// The name of the MCP Inspector container resource.
+ /// A reference to the for further configuration.
+ public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ var options = new McpInspectorOptions();
+
+ return builder.AddMcpInspector(name, options);
}
///
diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md b/src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md
index cdc6dc86..975aacb6 100644
--- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md
+++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md
@@ -23,7 +23,47 @@ 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
@@ -31,4 +71,4 @@ See the [official documentation](https://learn.microsoft.com/dotnet/aspire/commu
## Feedback & contributing
-https://github.com/CommunityToolkit/Aspire
+[https://github.com/CommunityToolkit/Aspire](https://github.com/CommunityToolkit/Aspire)
diff --git a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs
index 6ce1a0b3..054cf433 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs
@@ -1,19 +1,41 @@
+using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Testing;
namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests;
public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture>
{
- [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();
+ var mcpInspectorResource = appModel.Resources.OfType().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);
}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs
index b89600ac..98a1b163 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs
@@ -1,4 +1,5 @@
using Aspire.Hosting;
+using Aspire.Hosting.ApplicationModel;
namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests;
@@ -89,4 +90,184 @@ 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();
+
+ var inspectorResource = Assert.Single(appModel.Resources.OfType());
+ 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", options =>
+ {
+ options.ProxyToken = customToken;
+ });
+
+ using var app = appBuilder.Build();
+
+ // Assert
+ var appModel = app.Services.GetRequiredService();
+
+ var inspectorResource = Assert.Single(appModel.Resources.OfType());
+ 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", options =>
+ {
+ options.ClientPort = 1234;
+ options.ServerPort = 5678;
+ });
+
+ using var app = appBuilder.Build();
+
+ // Assert
+ var appModel = app.Services.GetRequiredService();
+ var inspectorResource = Assert.Single(appModel.Resources.OfType());
+
+ // Verify endpoints are configured correctly
+ var clientEndpoint = inspectorResource.Annotations.OfType()
+ .Single(e => e.Name == McpInspectorResource.ClientEndpointName);
+ var serverEndpoint = inspectorResource.Annotations.OfType()
+ .Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName);
+
+ Assert.Equal(1234, clientEndpoint.Port);
+ Assert.Equal(5678, serverEndpoint.Port);
+ }
+
+ [Fact]
+ public void AddMcpInspectorWithOptionsCreatesResourceCorrectly()
+ {
+ // Arrange
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var customToken = appBuilder.AddParameter("custom-token", secret: true);
+
+ var options = new McpInspectorOptions
+ {
+ ClientPort = 1111,
+ ServerPort = 2222,
+ InspectorVersion = "0.15.0",
+ ProxyToken = customToken
+ };
+
+ // Act
+ var inspector = appBuilder.AddMcpInspector("inspector", options);
+
+ using var app = appBuilder.Build();
+
+ // Assert
+ var appModel = app.Services.GetRequiredService();
+ var inspectorResource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.Equal("inspector", inspectorResource.Name);
+ Assert.Equal("custom-token", inspectorResource.ProxyTokenParameter.Name);
+ Assert.Same(customToken.Resource, inspectorResource.ProxyTokenParameter);
+
+ // Verify endpoints are configured correctly
+ var clientEndpoint = inspectorResource.Annotations.OfType()
+ .Single(e => e.Name == McpInspectorResource.ClientEndpointName);
+ var serverEndpoint = inspectorResource.Annotations.OfType()
+ .Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName);
+
+ Assert.Equal(1111, clientEndpoint.Port);
+ Assert.Equal(2222, serverEndpoint.Port);
+
+ // Verify version argument is set correctly
+ var argsAnnotation = inspectorResource.Annotations.OfType().First();
+ // We can't easily test the args directly, but we can verify the structure is correct
+ Assert.NotNull(argsAnnotation);
+ }
+
+ [Fact]
+ public void AddMcpInspectorWithOptionsUsesDefaultsWhenNotSpecified()
+ {
+ // Arrange
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var options = new McpInspectorOptions(); // Use all defaults
+
+ // Act
+ var inspector = appBuilder.AddMcpInspector("inspector", options);
+
+ using var app = appBuilder.Build();
+
+ // Assert
+ var appModel = app.Services.GetRequiredService();
+ var inspectorResource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.Equal("inspector", inspectorResource.Name);
+ Assert.Equal("inspector-proxyToken", inspectorResource.ProxyTokenParameter.Name);
+
+ // Verify endpoints use default ports
+ var clientEndpoint = inspectorResource.Annotations.OfType()
+ .Single(e => e.Name == McpInspectorResource.ClientEndpointName);
+ var serverEndpoint = inspectorResource.Annotations.OfType()
+ .Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName);
+
+ Assert.Equal(6274, clientEndpoint.Port);
+ Assert.Equal(6277, serverEndpoint.Port);
+ }
+
+ [Fact]
+ public void AddMcpInspectorWithConfigurationDelegateCreatesResourceCorrectly()
+ {
+ // Arrange
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var customToken = appBuilder.AddParameter("custom-token", secret: true);
+
+ // Act
+ var inspector = appBuilder.AddMcpInspector("inspector", options =>
+ {
+ options.ClientPort = 3333;
+ options.ServerPort = 4444;
+ options.InspectorVersion = "0.15.0";
+ options.ProxyToken = customToken;
+ });
+
+ using var app = appBuilder.Build();
+
+ // Assert
+ var appModel = app.Services.GetRequiredService();
+ var inspectorResource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.Equal("inspector", inspectorResource.Name);
+ Assert.Equal("custom-token", inspectorResource.ProxyTokenParameter.Name);
+ Assert.Same(customToken.Resource, inspectorResource.ProxyTokenParameter);
+
+ // Verify endpoints are configured correctly
+ var clientEndpoint = inspectorResource.Annotations.OfType()
+ .Single(e => e.Name == McpInspectorResource.ClientEndpointName);
+ var serverEndpoint = inspectorResource.Annotations.OfType()
+ .Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName);
+
+ Assert.Equal(3333, clientEndpoint.Port);
+ Assert.Equal(4444, serverEndpoint.Port);
+ }
}